アシアルブログ

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

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

こんにちは。笹亀です。

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

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

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

config/ProjectConfiguration.class.php


<?php
require_once dirname(__FILE__).'/../lib/vendor/symfony/lib/autoload/sfCoreAutoload.class.php';
sfCoreAutoload::register();

class ProjectConfiguration extends sfProjectConfiguration
{
  protected
    $masterConnection = null,
    $slaveConnection  = null;
 
  public function initializeConnections()
  {
    //Databaseへの接続情報を取得してコネクションのセット(Slave情報のみを取得)
    $file = sfConfig::get('sf_config_dir').'/database_slaves.yml';
    $config = file_exists($file) ? sfYaml::load($file) : array();
    $slave_connections  = array();
    foreach ($config['all'] as $name => $connection) {
      switch ($name) {
        case 'master':
          break;
        default:
          $dsn['slave'][] = $connection['param']['dsn'];
          break;
      }
    }
    
    //Slaveの振り分け処理(とりあえずはランダム選択
    $slave_num = rand(0,count($dsn['slave']) - 1);
    Doctrine_Manager::connection($dsn['slave'][$slave_num], 'slave');
    
    //Masterを必ずCurrentConnectionとしておく
    Doctrine_Manager::getInstance()->setCurrentConnection('master');
    
    //Slaveとマスタのコネクションのデフォルトセット
    $slaves = array();
    foreach (Doctrine_Manager::getInstance()->getConnections() as $name => $conn) {
      switch (true) {
        case 'master' == $name:
          $this->masterConnection = $conn;
          break;
        case 0 === strpos($name, 'slave'):
          $slaves[] = $conn;
          break;
      }
    }
    if (is_null($this->masterConnection)) {
      $this->masterConnection = Doctrine_Manager::connection();
    }
    
  }
 
  //Masterのコネクションを取得する
  public function getMasterConnection()
  {
    $this->masterConnection || $this->initializeConnections();
    return $this->masterConnection;
  }
 
  //Slaveのコネクションを取得する
  public function getSlaveConnection()
  {
    $this->slaveConnection || $this->initializeConnections();
    return $this->slaveConnection;
  }
  
  public function configureDoctrineConnection(Doctrine_Connection $conn)
  {
    $listener = new ConnectionListener(
      $this->getMasterConnection()->getDbh(),
      $this->getSlaveConnection()->getDbh()
    );
 
    $conn->addListener($listener);
  }

  
  public function setup()
  {
    $this->enablePlugins('sfDoctrinePlugin');
  }

  public function configureDoctrine($manager)
  {
    $manager->setAttribute(Doctrine::ATTR_USE_DQL_CALLBACKS, true);
  }
}


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

lib/ConnectionListener.class.php


<?php
 
class ConnectionListener extends Doctrine_Connection
  implements Doctrine_EventListener_Interface
{
  protected
    $master = null,
    $slave  = null;
 
  public function __construct(PDO $master, PDO $slave)
  {
    $this->master = $master;
    $this->slave  = $slave;
  }
 
  public function preQuery(Doctrine_Event $event)
  {
    //Transaction時は必ずmasterにコネクションをはるようにする
    //コネクション情報にmodulesにトランザクションがつかわれているPraivate変数がある
    //$conn->transaction->getState();  0 = sleep ,1 = active, 2 = busy
    $conn = $event->getInvoker();
    if ($conn->transaction->getState() == 0) {
      $this->forceDbh($conn, 'slave');
    } else {
      $this->forceDbh($conn, 'master');
    }
    
  }
 
  public function postQuery(Doctrine_Event $event)
  {
    $this->restoreDbh($event->getInvoker());
  }
 
  public function prePrepare(Doctrine_Event $event)
  {
    //コネクション情報を取得
    //トランザクション中かチェック()
    //$conn->transaction->getState();  0 = sleep ,1 = active, 2 = busy
    $conn = $event->getInvoker();
    if ($conn->transaction->getState() == 0) {
      $use = 0 === strpos(trim(strtolower($event->getQuery())), 'select') ?
        'slave' : 'master';
      $this->forceDbh($conn, $use);
    } else {
      $this->forceDbh($conn, 'master');
    }
  }
 
  public function postStmtExecute(Doctrine_Event $event)
  {
    $this->restoreDbh($event->getInvoker()->getConnection());
  }
 
  public function preExec(Doctrine_Event $event)
  {
    $this->forceDbh($event->getInvoker(), 'master');
  }
 
  public function postExec(Doctrine_Event $event)
  {
    $this->restoreDbh($event->getInvoker());
  }
 
  // protected
  protected function forceDbh($conn, $type)
  {
    if ($this->$type !== $conn->dbh)
    {
      $conn->options['previous_dbh'] = $conn->dbh;
      $conn->dbh = $this->$type;
    }
  }
 
  protected function restoreDbh($conn)
  {
    if (isset($conn->options['previous_dbh']))
    {
      $conn->dbh = $conn->options['previous_dbh'];
      unset($conn->options['previous_dbh']);
    }
  }
 //
 
  // the remaining methods required by Doctrine_EventListener_Interface
  public function preTransactionCommit(Doctrine_Event $event) { }
  public function postTransactionCommit(Doctrine_Event $event) { }
  public function preTransactionRollback(Doctrine_Event $event) { }
  public function postTransactionRollback(Doctrine_Event $event) { }
  public function preTransactionBegin(Doctrine_Event $event) { }
  public function postTransactionBegin(Doctrine_Event $event) { }
  public function postConnect(Doctrine_Event $event) { }
  public function preConnect(Doctrine_Event $event) { }
  public function postPrepare(Doctrine_Event $event) { }
  public function preStmtExecute(Doctrine_Event $event) { }
  public function preError(Doctrine_Event $event) { }
  public function postError(Doctrine_Event $event) { }
  public function preFetch(Doctrine_Event $event) { }
  public function postFetch(Doctrine_Event $event) { }
  public function preFetchAll(Doctrine_Event $event) { }
  public function postFetchAll(Doctrine_Event $event) { }
}


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

databases.yml


all:
  master:
    class: sfDoctrineDatabase
    param:
      dsn:      mysql:host=localhost;dbname=test
      username: user
      password: hogehoge



database_slaves.yml


all:
 slave_1:
    class: sfDoctrineDatabase
    param:
      dsn: mysql://user:hogehoge@localhost/test
 slave_2:
    class: sfDoctrineDatabase
    param:
      dsn: mysql://user:hogehoge@localhost/test


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

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

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