Commit e7d6fc2c authored by Benjamin Eberlei's avatar Benjamin Eberlei

DBAL-20 - MasterSlave Connection

parent f771cecc
...@@ -22,7 +22,7 @@ namespace Doctrine\DBAL\Connections; ...@@ -22,7 +22,7 @@ namespace Doctrine\DBAL\Connections;
use Doctrine\DBAL\Connection, use Doctrine\DBAL\Connection,
Doctrine\DBAL\Driver, Doctrine\DBAL\Driver,
Doctrine\ORM\Configuration, Doctrine\DBAL\Configuration,
Doctrine\Common\EventManager, Doctrine\Common\EventManager,
Doctrine\DBAL\Events; Doctrine\DBAL\Events;
...@@ -31,18 +31,35 @@ use Doctrine\DBAL\Connection, ...@@ -31,18 +31,35 @@ use Doctrine\DBAL\Connection,
* *
* Connection can be used with master-slave setups. * Connection can be used with master-slave setups.
* *
* Important for the understanding of this connection should be how it picks the slave and master. * Important for the understanding of this connection should be how and when
* it picks the slave or master.
* *
* 1. Master picked when 'exec', 'executeUpdate', 'insert', 'delete', 'update', 'createSavepoint', * 1. Slave if master was never picked before and ONLY if 'getWrappedConnection'
* 'releaseSavepoint', 'beginTransaction', 'rollback', 'commit' or 'getWrappedConnection' is called. * or 'executeQuery' is used.
* 2. If master was picked once during the lifetime of the connection it will always get picked afterwards. * 2. Master picked when 'exec', 'executeUpdate', 'insert', 'delete', 'update', 'createSavepoint',
* 3. Slave if master was never picked before and 'query', 'prepare' or 'executeQuery' is used. * 'releaseSavepoint', 'beginTransaction', 'rollback', 'commit', 'query' or
* 'prepare' is called.
* 3. If master was picked once during the lifetime of the connection it will always get picked afterwards.
* 4. One slave connection is randomly picked ONCE during a request.
* *
* ATTENTION: You can write to the slave with this connection if you execute a write query without * ATTENTION: You can write to the slave with this connection if you execute a write query without
* opening up a transaction. For example: * opening up a transaction. For example:
* *
* $conn = DriverManager::getConnection(...); * $conn = DriverManager::getConnection(...);
* $conn->query("DELETE FROM table"); * $conn->executeQuery("DELETE FROM table");
*
* Be aware that Connection#executeQuery is a method specifically for READ
* operations only.
*
* This connection is limited to slave operations using the
* Connection#executeQuery operation only, because it wouldn't be compatible
* with the ORM or SchemaManager code otherwise. Both use all the other
* operations in a context where writes could happen to a slave, which makes
* this restricted approach necessary.
*
* You can manually connect to the master at any time by calling:
*
* $conn->connect('master');
* *
* Instantiation through the DriverManager looks like: * Instantiation through the DriverManager looks like:
* *
...@@ -50,7 +67,8 @@ use Doctrine\DBAL\Connection, ...@@ -50,7 +67,8 @@ use Doctrine\DBAL\Connection,
* *
* $conn = DriverManager::getConnection(array( * $conn = DriverManager::getConnection(array(
* 'wrapperClass' => 'Doctrine\DBAL\Connections\MasterSlaveConnection', * 'wrapperClass' => 'Doctrine\DBAL\Connections\MasterSlaveConnection',
* 'master' => array('driver' => 'pdo_mysql', 'user' => '', 'password' => '', 'host' => '', 'dbname' => ''), * 'driver' => 'pdo_mysql',
* 'master' => array('user' => '', 'password' => '', 'host' => '', 'dbname' => ''),
* 'slaves' => array( * 'slaves' => array(
* array('user' => 'slave1', 'password', 'host' => '', 'dbname' => ''), * array('user' => 'slave1', 'password', 'host' => '', 'dbname' => ''),
* array('user' => 'slave2', 'password', 'host' => '', 'dbname' => ''), * array('user' => 'slave2', 'password', 'host' => '', 'dbname' => ''),
...@@ -84,8 +102,8 @@ class MasterSlaveConnection extends Connection ...@@ -84,8 +102,8 @@ class MasterSlaveConnection extends Connection
if ( !isset($params['slaves']) || !isset($params['master']) ) { if ( !isset($params['slaves']) || !isset($params['master']) ) {
throw new \InvalidArgumentException('master or slaves configuration missing'); throw new \InvalidArgumentException('master or slaves configuration missing');
} }
if ( count( array_filter($params['slaves'], function($v) { return !is_numeric($v); })) > 0 ) { if ( count($params['slaves']) == 0 ) {
throw new \InvalidArgumentException('You have to configure multiple slaves.'); throw new \InvalidArgumentException('You have to configure at least one slaves.');
} }
$params['master']['driver'] = $params['driver']; $params['master']['driver'] = $params['driver'];
...@@ -96,6 +114,16 @@ class MasterSlaveConnection extends Connection ...@@ -96,6 +114,16 @@ class MasterSlaveConnection extends Connection
parent::__construct($params, $driver, $config, $eventManager); parent::__construct($params, $driver, $config, $eventManager);
} }
/**
* Check if the connection is currently towards the master or not.
*
* @return bool
*/
public function isConnectedToMaster()
{
return $this->_conn !== null && $this->_conn === $this->connections['master'];
}
/** /**
* {@inheritDoc} * {@inheritDoc}
*/ */
...@@ -124,7 +152,7 @@ class MasterSlaveConnection extends Connection ...@@ -124,7 +152,7 @@ class MasterSlaveConnection extends Connection
if ($connectionName === 'master') { if ($connectionName === 'master') {
/** Set slave connection to master to avoid invalid reads */ /** Set slave connection to master to avoid invalid reads */
if ($this->connections['slave']) { if ($this->connections['slave']) {
$this->connections['slave']->close(); unset($this->connections['slave']);
} }
$this->connections['master'] = $this->connections['slave'] = $this->_conn = $this->connectTo($connectionName); $this->connections['master'] = $this->connections['slave'] = $this->_conn = $this->connectTo($connectionName);
...@@ -217,19 +245,19 @@ class MasterSlaveConnection extends Connection ...@@ -217,19 +245,19 @@ class MasterSlaveConnection extends Connection
/** /**
* {@inheritDoc} * {@inheritDoc}
*/ */
public function update($tableName, array $data, array $identifier) public function update($tableName, array $data, array $identifier, array $types = array())
{ {
$this->connect('master'); $this->connect('master');
return parent::update($tableName, $data, $identifier); return parent::update($tableName, $data, $identifier, $types);
} }
/** /**
* {@inheritDoc} * {@inheritDoc}
*/ */
public function insert($tableName, array $data) public function insert($tableName, array $data, array $types = array())
{ {
$this->connect('master'); $this->connect('master');
return parent::insert($tableName, $data); return parent::insert($tableName, $data, $types);
} }
/** /**
...@@ -241,16 +269,6 @@ class MasterSlaveConnection extends Connection ...@@ -241,16 +269,6 @@ class MasterSlaveConnection extends Connection
return parent::exec($statement); return parent::exec($statement);
} }
/**
* {@inheritDoc}
*/
public function getWrappedConnection()
{
$this->connect('master');
return $this->_conn;
}
/** /**
* {@inheritDoc} * {@inheritDoc}
*/ */
...@@ -280,4 +298,31 @@ class MasterSlaveConnection extends Connection ...@@ -280,4 +298,31 @@ class MasterSlaveConnection extends Connection
return parent::rollbackSavepoint($savepoint); return parent::rollbackSavepoint($savepoint);
} }
public function query()
{
$this->connect('master');
$args = func_get_args();
$logger = $this->getConfiguration()->getSQLLogger();
if ($logger) {
$logger->startQuery($args[0]);
}
$statement = call_user_func_array(array($this->_conn, 'query'), $args);
if ($logger) {
$logger->stopQuery();
}
return $statement;
}
public function prepare($statement)
{
$this->connect('master');
return parent::prepare($statement);
}
} }
...@@ -86,6 +86,9 @@ final class DriverManager ...@@ -86,6 +86,9 @@ final class DriverManager
* You may specify a custom wrapper class through the 'wrapperClass' * You may specify a custom wrapper class through the 'wrapperClass'
* parameter but this class MUST inherit from Doctrine\DBAL\Connection. * parameter but this class MUST inherit from Doctrine\DBAL\Connection.
* *
* <b>driverClass</b>:
* The driver class to use.
*
* @param array $params The parameters. * @param array $params The parameters.
* @param Doctrine\DBAL\Configuration The configuration to use. * @param Doctrine\DBAL\Configuration The configuration to use.
* @param Doctrine\Common\EventManager The event manager to use. * @param Doctrine\Common\EventManager The event manager to use.
......
<?php
namespace Doctrine\Tests\DBAL\Functional;
use Doctrine\Tests\DbalFunctionalTestCase;
use Doctrine\DBAL\DriverManager;
/**
* @group DBAL-20
*/
class MasterSlaveConnectionTest extends DbalFunctionalTestCase
{
public function setUp()
{
parent::setUp();
if ($this->_conn->getDatabasePlatform()->getName() == "sqlite") {
$this->markTestSkipped('Test does not work on sqlite.');
}
try {
/* @var $sm \Doctrine\DBAL\Schema\AbstractSchemaManager */
$table = new \Doctrine\DBAL\Schema\Table("master_slave_table");
$table->addColumn('test_int', 'integer');
$table->setPrimaryKey(array('test_int'));
$sm = $this->_conn->getSchemaManager();
$sm->createTable($table);
$this->_conn->insert('master_slave_table', array('test_int' => 1));
} catch(\Exception $e) {
}
}
public function createMasterSlaveConnection()
{
$params = $this->_conn->getParams();
$params['master'] = $params;
$params['slaves'] = array($params, $params);
$params['wrapperClass'] = 'Doctrine\DBAL\Connections\MasterSlaveConnection';
return DriverManager::getConnection($params);
}
public function testMasterOnConnect()
{
$conn = $this->createMasterSlaveConnection();
$this->assertFalse($conn->isConnectedToMaster());
$conn->connect('slave');
$this->assertFalse($conn->isConnectedToMaster());
$conn->connect('master');
$this->assertTrue($conn->isConnectedToMaster());
}
public function testNoMasterOnExecuteQuery()
{
$conn = $this->createMasterSlaveConnection();
$sql = "SELECT count(*) as num FROM master_slave_table";
$data = $conn->fetchAll($sql);
$data[0] = array_change_key_case($data[0], CASE_LOWER);
$this->assertEquals(1, $data[0]['num']);
$this->assertFalse($conn->isConnectedToMaster());
}
public function testMasterOnWriteOperation()
{
$conn = $this->createMasterSlaveConnection();
$conn->insert('master_slave_table', array('test_int' => 30));
$this->assertTrue($conn->isConnectedToMaster());
$sql = "SELECT count(*) as num FROM master_slave_table";
$data = $conn->fetchAll($sql);
$data[0] = array_change_key_case($data[0], CASE_LOWER);
$this->assertEquals(2, $data[0]['num']);
$this->assertTrue($conn->isConnectedToMaster());
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment