アシアルの中の人が技術と思いのたけをつづるブログ

DoctrineのMaster&Slaveのコネクションを操作するクラスを作成する方法

タグ [  symfony  Tech  Doctrine  ]
こんにちは。笹亀です。

symfonyはバージョン2かはSymfonyと頭文字が大文字表記となるとのことで、1.0のころに間違えてSymfonyと書いてツッコミを入れられたことを思い出しました。

さて本日はDoctrineのコネクションをMaster(更新 INSERT,UPDATE,DELETE)とSlave(選択 SELECT)で切り替えを行うProjectConfigurationとDoctrine_Connectionを継承したコネクションを操作するクラスを作成する方法について、ご紹介していきたいと思います。

ある程度の規模の開発をするときにどうしても必要になり、PropelにはあるのになぜDoctrineにはないのだと思い、いろいろソースとWebページを参考に調べながら作成しました。

config/ProjectConfiguration.class.php
  1. <?php
  2. require_once dirname(__FILE__).'/../lib/vendor/symfony/lib/autoload/sfCoreAutoload.class.php';
  3. sfCoreAutoload::register();
  4.  
  5. class ProjectConfiguration extends sfProjectConfiguration
  6. {
  7.   protected
  8.     $masterConnection = null,
  9.     $slaveConnection  = null;
  10.  
  11.   public function initializeConnections()
  12.   {
  13.     //Databaseへの接続情報を取得してコネクションのセット(Slave情報のみを取得)
  14.     $file = sfConfig::get('sf_config_dir').'/database_slaves.yml';
  15.     $config = file_exists($file) ? sfYaml::load($file) : array();
  16.     $slave_connections  = array();
  17.     foreach ($config['all'] as $name => $connection) {
  18.       switch ($name) {
  19.         case 'master':
  20.           break;
  21.         default:
  22.           $dsn['slave'][] = $connection['param']['dsn'];
  23.           break;
  24.       }
  25.     }
  26.     
  27.     //Slaveの振り分け処理(とりあえずはランダム選択
  28.     $slave_num = rand(0,count($dsn['slave']) - 1);
  29.     Doctrine_Manager::connection($dsn['slave'][$slave_num], 'slave');
  30.     
  31.     //Masterを必ずCurrentConnectionとしておく
  32.     Doctrine_Manager::getInstance()->setCurrentConnection('master');
  33.     
  34.     //Slaveとマスタのコネクションのデフォルトセット
  35.     $slaves = array();
  36.     foreach (Doctrine_Manager::getInstance()->getConnections() as $name => $conn) {
  37.       switch (true) {
  38.         case 'master' == $name:
  39.           $this->masterConnection = $conn;
  40.           break;
  41.         case 0 === strpos($name, 'slave'):
  42.           $slaves[] = $conn;
  43.           break;
  44.       }
  45.     }
  46.     if (is_null($this->masterConnection)) {
  47.       $this->masterConnection = Doctrine_Manager::connection();
  48.     }
  49.     
  50.   }
  51.  
  52.   //Masterのコネクションを取得する
  53.   public function getMasterConnection()
  54.   {
  55.     $this->masterConnection || $this->initializeConnections();
  56.     return $this->masterConnection;
  57.   }
  58.  
  59.   //Slaveのコネクションを取得する
  60.   public function getSlaveConnection()
  61.   {
  62.     $this->slaveConnection || $this->initializeConnections();
  63.     return $this->slaveConnection;
  64.   }
  65.   
  66.   public function configureDoctrineConnection(Doctrine_Connection $conn)
  67.   {
  68.     $listener = new ConnectionListener(
  69.       $this->getMasterConnection()->getDbh(),
  70.       $this->getSlaveConnection()->getDbh()
  71.     );
  72.  
  73.     $conn->addListener($listener);
  74.   }
  75.  
  76.   
  77.   public function setup()
  78.   {
  79.     $this->enablePlugins('sfDoctrinePlugin');
  80.   }
  81.  
  82.   public function configureDoctrine($manager)
  83.   {
  84.     $manager->setAttribute(Doctrine::ATTR_USE_DQL_CALLBACKS, true);
  85.   }
  86. }

Doctrine_EventListener_Interfaceのimplementsとして作成します。preQuery,prePrepareをフックして使用するコネクションを変更します。トランザクション処理をしているときは必ずMaster側のコネクションを使用することを気をつけながら実装が必要です。

lib/ConnectionListener.class.php
  1. <?php
  2.  
  3. class ConnectionListener extends Doctrine_Connection
  4.   implements Doctrine_EventListener_Interface
  5. {
  6.   protected
  7.     $master = null,
  8.     $slave  = null;
  9.  
  10.   public function __construct(PDO $master, PDO $slave)
  11.   {
  12.     $this->master = $master;
  13.     $this->slave  = $slave;
  14.   }
  15.  
  16.   public function preQuery(Doctrine_Event $event)
  17.   {
  18.     //Transaction時は必ずmasterにコネクションをはるようにする
  19.     //コネクション情報にmodulesにトランザクションがつかわれているPraivate変数がある
  20.     //$conn->transaction->getState();  0 = sleep ,1 = active, 2 = busy
  21.     $conn = $event->getInvoker();
  22.     if ($conn->transaction->getState() == 0) {
  23.       $this->forceDbh($conn, 'slave');
  24.     } else {
  25.       $this->forceDbh($conn, 'master');
  26.     }
  27.     
  28.   }
  29.  
  30.   public function postQuery(Doctrine_Event $event)
  31.   {
  32.     $this->restoreDbh($event->getInvoker());
  33.   }
  34.  
  35.   public function prePrepare(Doctrine_Event $event)
  36.   {
  37.     //コネクション情報を取得
  38.     //トランザクション中かチェック()
  39.     //$conn->transaction->getState();  0 = sleep ,1 = active, 2 = busy
  40.     $conn = $event->getInvoker();
  41.     if ($conn->transaction->getState() == 0) {
  42.       $use = 0 === strpos(trim(strtolower($event->getQuery())), 'select') ?
  43.         'slave' : 'master';
  44.       $this->forceDbh($conn, $use);
  45.     } else {
  46.       $this->forceDbh($conn, 'master');
  47.     }
  48.   }
  49.  
  50.   public function postStmtExecute(Doctrine_Event $event)
  51.   {
  52.     $this->restoreDbh($event->getInvoker()->getConnection());
  53.   }
  54.  
  55.   public function preExec(Doctrine_Event $event)
  56.   {
  57.     $this->forceDbh($event->getInvoker(), 'master');
  58.   }
  59.  
  60.   public function postExec(Doctrine_Event $event)
  61.   {
  62.     $this->restoreDbh($event->getInvoker());
  63.   }
  64.  
  65.   // protected
  66.   protected function forceDbh($conn, $type)
  67.   {
  68.     if ($this->$type !== $conn->dbh)
  69.     {
  70.       $conn->options['previous_dbh'] = $conn->dbh;
  71.       $conn->dbh = $this->$type;
  72.     }
  73.   }
  74.  
  75.   protected function restoreDbh($conn)
  76.   {
  77.     if (isset($conn->options['previous_dbh']))
  78.     {
  79.       $conn->dbh = $conn->options['previous_dbh'];
  80.       unset($conn->options['previous_dbh']);
  81.     }
  82.   }
  83.  //
  84.  
  85.   // the remaining methods required by Doctrine_EventListener_Interface
  86.   public function preTransactionCommit(Doctrine_Event $event) { }
  87.   public function postTransactionCommit(Doctrine_Event $event) { }
  88.   public function preTransactionRollback(Doctrine_Event $event) { }
  89.   public function postTransactionRollback(Doctrine_Event $event) { }
  90.   public function preTransactionBegin(Doctrine_Event $event) { }
  91.   public function postTransactionBegin(Doctrine_Event $event) { }
  92.   public function postConnect(Doctrine_Event $event) { }
  93.   public function preConnect(Doctrine_Event $event) { }
  94.   public function postPrepare(Doctrine_Event $event) { }
  95.   public function preStmtExecute(Doctrine_Event $event) { }
  96.   public function preError(Doctrine_Event $event) { }
  97.   public function postError(Doctrine_Event $event) { }
  98.   public function preFetch(Doctrine_Event $event) { }
  99.   public function postFetch(Doctrine_Event $event) { }
  100.   public function preFetchAll(Doctrine_Event $event) { }
  101.   public function postFetchAll(Doctrine_Event $event) { }
  102. }

データベースの接続情報を作成、変更します。databases.ymlだけではどうしてもうまく実装ができなかったので、Masterはdatabases.ymlに記載して、slaveはdatabase_slaves.ymlを作成して設置することにしました。

databases.yml
  1. all:
  2.   master:
  3.     class: sfDoctrineDatabase
  4.     param:
  5.       dsn:      mysql:host=localhost;dbname=test
  6.       username: user
  7.       password: hogehoge


database_slaves.yml
  1. all:
  2.  slave_1:
  3.     class: sfDoctrineDatabase
  4.     param:
  5.       dsn: mysql://user:hogehoge@localhost/test
  6.  slave_2:
  7.     class: sfDoctrineDatabase
  8.     param:
  9.       dsn: mysql://user:hogehoge@localhost/test

作成当時はまだDocrtineのMaster&Slave構成のConnection操作に対応をしておりませんでしたが、2010年2月24日にプラグインとして正式にリリースされました。
http://www.symfony-project.org/plugins/sfDoctrineMasterSlavePlugin

自分が作成するときに参考にさせていただいたsymfonyのプロジェクトチームのKris Wallsmith氏がリリースしたプラグインですので、信頼してご利用いただけるのではないでしょうか。

※この前、試しに使用してみたらうまく動かすことができませんでした;;
動かしたことがある方、是非とも教えていただけますと幸いです。

コメントフォーム

認証
captcha_key
 

トラックバック