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氏がリリースしたプラグインですので、信頼してご利用いただけるのではないでしょうか。
※この前、試しに使用してみたらうまく動かすことができませんでした;;
動かしたことがある方、是非とも教えていただけますと幸いです。