diff --git a/lib/Doctrine/DBAL/Connections/MasterSlaveConnection.php b/lib/Doctrine/DBAL/Connections/MasterSlaveConnection.php index 9362bc0c636..de47afbab87 100644 --- a/lib/Doctrine/DBAL/Connections/MasterSlaveConnection.php +++ b/lib/Doctrine/DBAL/Connections/MasterSlaveConnection.php @@ -4,88 +4,20 @@ use Doctrine\Common\EventManager; use Doctrine\DBAL\Configuration; -use Doctrine\DBAL\Connection; use Doctrine\DBAL\Driver; -use Doctrine\DBAL\Driver\Connection as DriverConnection; -use Doctrine\DBAL\Event\ConnectionEventArgs; -use Doctrine\DBAL\Events; use InvalidArgumentException; -use function array_rand; -use function assert; -use function count; -use function func_get_args; +use function sprintf; +use function trigger_error; +use const E_USER_DEPRECATED; /** - * Master-Slave Connection - * - * Connection can be used with master-slave setups. - * - * Important for the understanding of this connection should be how and when - * it picks the slave or master. - * - * 1. Slave if master was never picked before and ONLY if 'getWrappedConnection' - * or 'executeQuery' is used. - * 2. Master picked when 'exec', 'executeUpdate', 'insert', 'delete', 'update', 'createSavepoint', - * '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 - * opening up a transaction. For example: - * - * $conn = DriverManager::getConnection(...); - * $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: - * - * @example - * - * $conn = DriverManager::getConnection(array( - * 'wrapperClass' => 'Doctrine\DBAL\Connections\MasterSlaveConnection', - * 'driver' => 'pdo_mysql', - * 'master' => array('user' => '', 'password' => '', 'host' => '', 'dbname' => ''), - * 'slaves' => array( - * array('user' => 'slave1', 'password', 'host' => '', 'dbname' => ''), - * array('user' => 'slave2', 'password', 'host' => '', 'dbname' => ''), - * ) - * )); - * - * You can also pass 'driverOptions' and any other documented option to each of this drivers to pass additional information. + * @deprecated Use PrimaryReplicaConnection instead */ -class MasterSlaveConnection extends Connection +class MasterSlaveConnection extends PrimaryReplicaConnection { /** - * Master and slave connection (one of the randomly picked slaves). - * - * @var DriverConnection[]|null[] - */ - protected $connections = ['master' => null, 'slave' => null]; - - /** - * You can keep the slave connection and then switch back to it - * during the request if you know what you are doing. - * - * @var bool - */ - protected $keepSlave = false; - - /** - * Creates Master Slave Connection. + * Creates Primary Replica Connection. * * @param mixed[] $params * @@ -93,289 +25,73 @@ class MasterSlaveConnection extends Connection */ public function __construct(array $params, Driver $driver, ?Configuration $config = null, ?EventManager $eventManager = null) { - if (! isset($params['slaves'], $params['master'])) { - throw new InvalidArgumentException('master or slaves configuration missing'); - } + $this->deprecated(self::class, PrimaryReplicaConnection::class); - if (count($params['slaves']) === 0) { - throw new InvalidArgumentException('You have to configure at least one slaves.'); - } + if (isset($params['master'])) { + $this->deprecated("Params key 'master'", "'primary'"); - $params['master']['driver'] = $params['driver']; - foreach ($params['slaves'] as $slaveKey => $slave) { - $params['slaves'][$slaveKey]['driver'] = $params['driver']; + $params['primary'] = $params['master']; + unset($params['master']); } - $this->keepSlave = (bool) ($params['keepSlave'] ?? false); + if (isset($params['slaves'])) { + $this->deprecated("Params key 'slaves'", "'replica'"); - parent::__construct($params, $driver, $config, $eventManager); - } - - /** - * Checks if the connection is currently towards the master or not. - * - * @return bool - */ - public function isConnectedToMaster() - { - return $this->_conn !== null && $this->_conn === $this->connections['master']; - } - - /** - * @param string|null $connectionName - * - * @return bool - */ - public function connect($connectionName = null) - { - $requestedConnectionChange = ($connectionName !== null); - $connectionName = $connectionName ?: 'slave'; - - if ($connectionName !== 'slave' && $connectionName !== 'master') { - throw new InvalidArgumentException('Invalid option to connect(), only master or slave allowed.'); - } - - // If we have a connection open, and this is not an explicit connection - // change request, then abort right here, because we are already done. - // This prevents writes to the slave in case of "keepSlave" option enabled. - if ($this->_conn !== null && ! $requestedConnectionChange) { - return false; - } - - $forceMasterAsSlave = false; - - if ($this->getTransactionNestingLevel() > 0) { - $connectionName = 'master'; - $forceMasterAsSlave = true; + $params['replica'] = $params['slave']; + unset($params['slave']); } - if (isset($this->connections[$connectionName])) { - $this->_conn = $this->connections[$connectionName]; + if (isset($params['keepSlave'])) { + $this->deprecated("Params key 'keepSlave'", "'keepReplica'"); - if ($forceMasterAsSlave && ! $this->keepSlave) { - $this->connections['slave'] = $this->_conn; - } - - return false; + $params['keepReplica'] = $params['keepSlave']; + unset($params['keepSlave']); } - if ($connectionName === 'master') { - $this->connections['master'] = $this->_conn = $this->connectTo($connectionName); - - // Set slave connection to master to avoid invalid reads - if (! $this->keepSlave) { - $this->connections['slave'] = $this->connections['master']; - } - } else { - $this->connections['slave'] = $this->_conn = $this->connectTo($connectionName); - } - - if ($this->_eventManager->hasListeners(Events::postConnect)) { - $eventArgs = new ConnectionEventArgs($this); - $this->_eventManager->dispatchEvent(Events::postConnect, $eventArgs); - } - - return true; + parent::__construct($params, $driver, $config, $eventManager); } /** - * Connects to a specific connection. - * - * @param string $connectionName - * - * @return DriverConnection + * Checks if the connection is currently towards the primary or not. */ - protected function connectTo($connectionName) + public function isConnectedToMaster() : bool { - $params = $this->getParams(); - - $driverOptions = $params['driverOptions'] ?? []; + $this->deprecated('isConnectedtoMaster()', 'isConnectedToPrimary()'); - $connectionParams = $this->chooseConnectionConfiguration($connectionName, $params); - - $user = $connectionParams['user'] ?? null; - $password = $connectionParams['password'] ?? null; - - return $this->_driver->connect($connectionParams, $user, $password, $driverOptions); + return $this->isConnectedToPrimary(); } /** - * @param string $connectionName - * @param mixed[] $params + * @param string|null $connectionName * - * @return mixed + * @return bool */ - protected function chooseConnectionConfiguration($connectionName, $params) + public function connect($connectionName = null) { if ($connectionName === 'master') { - return $params['master']; - } - - $config = $params['slaves'][array_rand($params['slaves'])]; - - if (! isset($config['charset']) && isset($params['master']['charset'])) { - $config['charset'] = $params['master']['charset']; - } - - return $config; - } - - /** - * {@inheritDoc} - */ - public function executeUpdate($query, array $params = [], array $types = []) - { - $this->connect('master'); - - return parent::executeUpdate($query, $params, $types); - } - - /** - * {@inheritDoc} - */ - public function beginTransaction() - { - $this->connect('master'); - - return parent::beginTransaction(); - } - - /** - * {@inheritDoc} - */ - public function commit() - { - $this->connect('master'); - - return parent::commit(); - } - - /** - * {@inheritDoc} - */ - public function rollBack() - { - $this->connect('master'); + $connectionName = 'primary'; - return parent::rollBack(); - } - - /** - * {@inheritDoc} - */ - public function delete($tableName, array $identifier, array $types = []) - { - $this->connect('master'); - - return parent::delete($tableName, $identifier, $types); - } - - /** - * {@inheritDoc} - */ - public function close() - { - unset($this->connections['master'], $this->connections['slave']); - - parent::close(); - - $this->_conn = null; - $this->connections = ['master' => null, 'slave' => null]; - } - - /** - * {@inheritDoc} - */ - public function update($tableName, array $data, array $identifier, array $types = []) - { - $this->connect('master'); - - return parent::update($tableName, $data, $identifier, $types); - } - - /** - * {@inheritDoc} - */ - public function insert($tableName, array $data, array $types = []) - { - $this->connect('master'); - - return parent::insert($tableName, $data, $types); - } - - /** - * {@inheritDoc} - */ - public function exec($statement) - { - $this->connect('master'); - - return parent::exec($statement); - } - - /** - * {@inheritDoc} - */ - public function createSavepoint($savepoint) - { - $this->connect('master'); - - parent::createSavepoint($savepoint); - } - - /** - * {@inheritDoc} - */ - public function releaseSavepoint($savepoint) - { - $this->connect('master'); - - parent::releaseSavepoint($savepoint); - } - - /** - * {@inheritDoc} - */ - public function rollbackSavepoint($savepoint) - { - $this->connect('master'); - - parent::rollbackSavepoint($savepoint); - } - - /** - * {@inheritDoc} - */ - public function query() - { - $this->connect('master'); - assert($this->_conn instanceof DriverConnection); - - $args = func_get_args(); - - $logger = $this->getConfiguration()->getSQLLogger(); - if ($logger) { - $logger->startQuery($args[0]); + $this->deprecated('connect("master")', 'connect("primary")'); } - $statement = $this->_conn->query(...$args); + if ($connectionName === 'slave') { + $connectionName = 'replica'; - $statement->setFetchMode($this->defaultFetchMode); - - if ($logger) { - $logger->stopQuery(); + $this->deprecated('connect("slave")', 'connect("replica")'); } - return $statement; + return parent::connect($connectionName); } - /** - * {@inheritDoc} - */ - public function prepare($statement) + private function deprecated(string $thing, string $instead) : void { - $this->connect('master'); - - return parent::prepare($statement); + @trigger_error( + sprintf( + '%s is deprecated since doctrine/dbal 2.10 and will be removed in 3.0, use %s instead.', + $thing, + $instead + ), + E_USER_DEPRECATED + ); } } diff --git a/lib/Doctrine/DBAL/Connections/PrimaryReplicaConnection.php b/lib/Doctrine/DBAL/Connections/PrimaryReplicaConnection.php new file mode 100644 index 00000000000..f14c1b158c2 --- /dev/null +++ b/lib/Doctrine/DBAL/Connections/PrimaryReplicaConnection.php @@ -0,0 +1,380 @@ +executeQuery("DELETE FROM table"); + * + * Be aware that Connection#executeQuery is a method specifically for READ + * operations only. + * + * This connection is limited to replica 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 replica, which makes + * this restricted approach necessary. + * + * You can manually connect to the primary at any time by calling: + * + * $conn->connect('primary'); + * + * Instantiation through the DriverManager looks like: + * + * @example + * + * $conn = DriverManager::getConnection(array( + * 'wrapperClass' => 'Doctrine\DBAL\Connections\PrimaryReplicaConnection', + * 'driver' => 'pdo_mysql', + * 'primary' => array('user' => '', 'password' => '', 'host' => '', 'dbname' => ''), + * 'replica' => array( + * array('user' => 'replica1', 'password', 'host' => '', 'dbname' => ''), + * array('user' => 'replica2', 'password', 'host' => '', 'dbname' => ''), + * ) + * )); + * + * You can also pass 'driverOptions' and any other documented option to each of this drivers to pass additional information. + */ +class PrimaryReplicaConnection extends Connection +{ + /** + * Primary and Replica connection (one of the randomly picked replicas). + * + * @var DriverConnection[]|null[] + */ + protected $connections = ['primary' => null, 'replica' => null]; + + /** + * You can keep the replica connection and then switch back to it + * during the request if you know what you are doing. + * + * @var bool + */ + protected $keepReplica = false; + + /** + * Creates Primary Replica Connection. + * + * @param mixed[] $params + * + * @throws InvalidArgumentException + */ + public function __construct(array $params, Driver $driver, ?Configuration $config = null, ?EventManager $eventManager = null) + { + if (! isset($params['replica'], $params['primary'])) { + throw new InvalidArgumentException('primary or replica configuration missing'); + } + + if (count($params['replica']) === 0) { + throw new InvalidArgumentException('You have to configure at least one replica.'); + } + + $params['primary']['driver'] = $params['driver']; + foreach ($params['replica'] as $replicaKey => $replica) { + $params['replica'][$replicaKey]['driver'] = $params['driver']; + } + + $this->keepReplica = (bool) ($params['keepReplica'] ?? false); + + parent::__construct($params, $driver, $config, $eventManager); + } + + /** + * Checks if the connection is currently towards the primary or not. + * + * @return bool + */ + public function isConnectedToPrimary() + { + return $this->_conn !== null && $this->_conn === $this->connections['primary']; + } + + /** + * @param string|null $connectionName + * + * @return bool + */ + public function connect($connectionName = null) + { + $requestedConnectionChange = ($connectionName !== null); + $connectionName = $connectionName ?: 'replica'; + + if ($connectionName !== 'replica' && $connectionName !== 'primary') { + throw new InvalidArgumentException('Invalid option to connect(), only primary or replica allowed.'); + } + + // If we have a connection open, and this is not an explicit connection + // change request, then abort right here, because we are already done. + // This prevents writes to the replica in case of "keepReplica" option enabled. + if ($this->_conn !== null && ! $requestedConnectionChange) { + return false; + } + + $forcePrimaryAsReplica = false; + + if ($this->getTransactionNestingLevel() > 0) { + $connectionName = 'primary'; + $forcePrimaryAsReplica = true; + } + + if (isset($this->connections[$connectionName])) { + $this->_conn = $this->connections[$connectionName]; + + if ($forcePrimaryAsReplica && ! $this->keepReplica) { + $this->connections['replica'] = $this->_conn; + } + + return false; + } + + if ($connectionName === 'primary') { + $this->connections['primary'] = $this->_conn = $this->connectTo($connectionName); + + // Set replica connection to primary to avoid invalid reads + if (! $this->keepReplica) { + $this->connections['replica'] = $this->connections['primary']; + } + } else { + $this->connections['replica'] = $this->_conn = $this->connectTo($connectionName); + } + + if ($this->_eventManager->hasListeners(Events::postConnect)) { + $eventArgs = new ConnectionEventArgs($this); + $this->_eventManager->dispatchEvent(Events::postConnect, $eventArgs); + } + + return true; + } + + /** + * Connects to a specific connection. + * + * @param string $connectionName + * + * @return DriverConnection + */ + protected function connectTo($connectionName) + { + $params = $this->getParams(); + + $driverOptions = $params['driverOptions'] ?? []; + + $connectionParams = $this->chooseConnectionConfiguration($connectionName, $params); + + $user = $connectionParams['user'] ?? null; + $password = $connectionParams['password'] ?? null; + + return $this->_driver->connect($connectionParams, $user, $password, $driverOptions); + } + + /** + * @param string $connectionName + * @param mixed[] $params + * + * @return mixed + */ + protected function chooseConnectionConfiguration($connectionName, $params) + { + if ($connectionName === 'primary') { + return $params['primary']; + } + + $config = $params['replica'][array_rand($params['replica'])]; + + if (! isset($config['charset']) && isset($params['primary']['charset'])) { + $config['charset'] = $params['primary']['charset']; + } + + return $config; + } + + /** + * {@inheritDoc} + */ + public function executeUpdate($query, array $params = [], array $types = []) + { + $this->connect('primary'); + + return parent::executeUpdate($query, $params, $types); + } + + /** + * {@inheritDoc} + */ + public function beginTransaction() + { + $this->connect('primary'); + + return parent::beginTransaction(); + } + + /** + * {@inheritDoc} + */ + public function commit() + { + $this->connect('primary'); + + return parent::commit(); + } + + /** + * {@inheritDoc} + */ + public function rollBack() + { + $this->connect('primary'); + + return parent::rollBack(); + } + + /** + * {@inheritDoc} + */ + public function delete($tableName, array $identifier, array $types = []) + { + $this->connect('primary'); + + return parent::delete($tableName, $identifier, $types); + } + + /** + * {@inheritDoc} + */ + public function close() + { + unset($this->connections['primary'], $this->connections['replica']); + + parent::close(); + + $this->_conn = null; + $this->connections = ['primary' => null, 'replica' => null]; + } + + /** + * {@inheritDoc} + */ + public function update($tableName, array $data, array $identifier, array $types = []) + { + $this->connect('primary'); + + return parent::update($tableName, $data, $identifier, $types); + } + + /** + * {@inheritDoc} + */ + public function insert($tableName, array $data, array $types = []) + { + $this->connect('primary'); + + return parent::insert($tableName, $data, $types); + } + + /** + * {@inheritDoc} + */ + public function exec($statement) + { + $this->connect('primary'); + + return parent::exec($statement); + } + + /** + * {@inheritDoc} + */ + public function createSavepoint($savepoint) + { + $this->connect('primary'); + + parent::createSavepoint($savepoint); + } + + /** + * {@inheritDoc} + */ + public function releaseSavepoint($savepoint) + { + $this->connect('primary'); + + parent::releaseSavepoint($savepoint); + } + + /** + * {@inheritDoc} + */ + public function rollbackSavepoint($savepoint) + { + $this->connect('primary'); + + parent::rollbackSavepoint($savepoint); + } + + /** + * {@inheritDoc} + */ + public function query() + { + $this->connect('primary'); + assert($this->_conn instanceof DriverConnection); + + $args = func_get_args(); + + $logger = $this->getConfiguration()->getSQLLogger(); + if ($logger) { + $logger->startQuery($args[0]); + } + + $statement = $this->_conn->query(...$args); + + $statement->setFetchMode($this->defaultFetchMode); + + if ($logger) { + $logger->stopQuery(); + } + + return $statement; + } + + /** + * {@inheritDoc} + */ + public function prepare($statement) + { + $this->connect('primary'); + + return parent::prepare($statement); + } +} diff --git a/lib/Doctrine/DBAL/DriverManager.php b/lib/Doctrine/DBAL/DriverManager.php index f7462bbd462..b56ac7f9054 100644 --- a/lib/Doctrine/DBAL/DriverManager.php +++ b/lib/Doctrine/DBAL/DriverManager.php @@ -142,17 +142,29 @@ public static function getConnection( $params = self::parseDatabaseUrl($params); - // URL support for MasterSlaveConnection + // @todo: deprecated, notice thrown by connection constructor if (isset($params['master'])) { $params['master'] = self::parseDatabaseUrl($params['master']); } + // @todo: deprecated, notice thrown by connection constructor if (isset($params['slaves'])) { foreach ($params['slaves'] as $key => $slaveParams) { $params['slaves'][$key] = self::parseDatabaseUrl($slaveParams); } } + // URL support for PrimaryReplicaConnection + if (isset($params['primary'])) { + $params['primary'] = self::parseDatabaseUrl($params['primary']); + } + + if (isset($params['replica'])) { + foreach ($params['replica'] as $key => $replicaParams) { + $params['replica'][$key] = self::parseDatabaseUrl($replicaParams); + } + } + // URL support for PoolingShardConnection if (isset($params['global'])) { $params['global'] = self::parseDatabaseUrl($params['global']); diff --git a/tests/Doctrine/Tests/DBAL/DriverManagerTest.php b/tests/Doctrine/Tests/DBAL/DriverManagerTest.php index 524e03031c4..1b243270494 100644 --- a/tests/Doctrine/Tests/DBAL/DriverManagerTest.php +++ b/tests/Doctrine/Tests/DBAL/DriverManagerTest.php @@ -3,7 +3,7 @@ namespace Doctrine\Tests\DBAL; use Doctrine\DBAL\Connection; -use Doctrine\DBAL\Connections\MasterSlaveConnection; +use Doctrine\DBAL\Connections\PrimaryReplicaConnection; use Doctrine\DBAL\DBALException; use Doctrine\DBAL\Driver; use Doctrine\DBAL\Driver\DrizzlePDOMySql\Driver as DrizzlePDOMySqlDriver; @@ -139,15 +139,15 @@ public function testValidDriverClass(): void self::assertInstanceOf(PDOMySQLDriver::class, $conn->getDriver()); } - public function testDatabaseUrlMasterSlave(): void + public function testDatabaseUrlPrimaryReplica() : void { $options = [ 'driver' => 'pdo_mysql', - 'master' => ['url' => 'mysql://foo:bar@localhost:11211/baz'], - 'slaves' => [ - 'slave1' => ['url' => 'mysql://foo:bar@localhost:11211/baz_slave'], + 'primary' => ['url' => 'mysql://foo:bar@localhost:11211/baz'], + 'replica' => [ + 'replica1' => ['url' => 'mysql://foo:bar@localhost:11211/baz_replica'], ], - 'wrapperClass' => MasterSlaveConnection::class, + 'wrapperClass' => PrimaryReplicaConnection::class, ]; $conn = DriverManager::getConnection($options); @@ -163,12 +163,12 @@ public function testDatabaseUrlMasterSlave(): void ]; foreach ($expected as $key => $value) { - self::assertEquals($value, $params['master'][$key]); - self::assertEquals($value, $params['slaves']['slave1'][$key]); + self::assertEquals($value, $params['primary'][$key]); + self::assertEquals($value, $params['replica']['replica1'][$key]); } - self::assertEquals('baz', $params['master']['dbname']); - self::assertEquals('baz_slave', $params['slaves']['slave1']['dbname']); + self::assertEquals('baz', $params['primary']['dbname']); + self::assertEquals('baz_replica', $params['replica']['replica1']['dbname']); } public function testDatabaseUrlShard(): void diff --git a/tests/Doctrine/Tests/DBAL/Functional/MasterSlaveConnectionTest.php b/tests/Doctrine/Tests/DBAL/Functional/MasterSlaveConnectionTest.php deleted file mode 100644 index ac7d84d030e..00000000000 --- a/tests/Doctrine/Tests/DBAL/Functional/MasterSlaveConnectionTest.php +++ /dev/null @@ -1,243 +0,0 @@ -connection->getDatabasePlatform()->getName(); - - // This is a MySQL specific test, skip other vendors. - if ($platformName !== 'mysql') { - $this->markTestSkipped(sprintf('Test does not work on %s.', $platformName)); - } - - try { - $table = new Table('master_slave_table'); - $table->addColumn('test_int', 'integer'); - $table->setPrimaryKey(['test_int']); - - $sm = $this->connection->getSchemaManager(); - $sm->createTable($table); - } catch (Throwable $e) { - } - - $this->connection->executeUpdate('DELETE FROM master_slave_table'); - $this->connection->insert('master_slave_table', ['test_int' => 1]); - } - - private function createMasterSlaveConnection(bool $keepSlave = false): MasterSlaveConnection - { - return DriverManager::getConnection($this->createMasterSlaveConnectionParams($keepSlave)); - } - - /** - * @return mixed[] - */ - private function createMasterSlaveConnectionParams(bool $keepSlave = false): array - { - $params = $this->connection->getParams(); - $params['master'] = $params; - $params['slaves'] = [$params, $params]; - $params['keepSlave'] = $keepSlave; - $params['wrapperClass'] = MasterSlaveConnection::class; - - return $params; - } - - public function testInheritCharsetFromMaster(): void - { - $charsets = [ - 'utf8', - 'latin1', - ]; - - foreach ($charsets as $charset) { - $params = $this->createMasterSlaveConnectionParams(); - $params['master']['charset'] = $charset; - - foreach ($params['slaves'] as $index => $slaveParams) { - if (! isset($slaveParams['charset'])) { - continue; - } - - unset($params['slaves'][$index]['charset']); - } - - $conn = DriverManager::getConnection($params); - self::assertInstanceOf(MasterSlaveConnection::class, $conn); - $conn->connect('slave'); - - self::assertFalse($conn->isConnectedToMaster()); - - $clientCharset = $conn->fetchColumn('select @@character_set_client as c'); - - self::assertSame( - $charset, - substr(strtolower($clientCharset), 0, strlen($charset)) - ); - } - } - - public function testMasterOnConnect(): void - { - $conn = $this->createMasterSlaveConnection(); - - self::assertFalse($conn->isConnectedToMaster()); - $conn->connect('slave'); - self::assertFalse($conn->isConnectedToMaster()); - $conn->connect('master'); - self::assertTrue($conn->isConnectedToMaster()); - } - - public function testNoMasterOnExecuteQuery(): void - { - $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); - - self::assertEquals(1, $data[0]['num']); - self::assertFalse($conn->isConnectedToMaster()); - } - - public function testMasterOnWriteOperation(): void - { - $conn = $this->createMasterSlaveConnection(); - $conn->insert('master_slave_table', ['test_int' => 30]); - - self::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); - - self::assertEquals(2, $data[0]['num']); - self::assertTrue($conn->isConnectedToMaster()); - } - - /** - * @group DBAL-335 - */ - public function testKeepSlaveBeginTransactionStaysOnMaster(): void - { - $conn = $this->createMasterSlaveConnection($keepSlave = true); - $conn->connect('slave'); - - $conn->beginTransaction(); - $conn->insert('master_slave_table', ['test_int' => 30]); - $conn->commit(); - - self::assertTrue($conn->isConnectedToMaster()); - - $conn->connect(); - self::assertTrue($conn->isConnectedToMaster()); - - $conn->connect('slave'); - self::assertFalse($conn->isConnectedToMaster()); - } - - /** - * @group DBAL-335 - */ - public function testKeepSlaveInsertStaysOnMaster(): void - { - $conn = $this->createMasterSlaveConnection($keepSlave = true); - $conn->connect('slave'); - - $conn->insert('master_slave_table', ['test_int' => 30]); - - self::assertTrue($conn->isConnectedToMaster()); - - $conn->connect(); - self::assertTrue($conn->isConnectedToMaster()); - - $conn->connect('slave'); - self::assertFalse($conn->isConnectedToMaster()); - } - - public function testMasterSlaveConnectionCloseAndReconnect(): void - { - $conn = $this->createMasterSlaveConnection(); - $conn->connect('master'); - self::assertTrue($conn->isConnectedToMaster()); - - $conn->close(); - self::assertFalse($conn->isConnectedToMaster()); - - $conn->connect('master'); - self::assertTrue($conn->isConnectedToMaster()); - } - - public function testQueryOnMaster(): void - { - $conn = $this->createMasterSlaveConnection(); - - $query = 'SELECT count(*) as num FROM master_slave_table'; - - $statement = $conn->query($query); - - self::assertInstanceOf(Statement::class, $statement); - - //Query must be executed only on Master - self::assertTrue($conn->isConnectedToMaster()); - - $data = $statement->fetchAll(); - - //Default fetchmode is FetchMode::ASSOCIATIVE - self::assertArrayHasKey(0, $data); - self::assertArrayHasKey('num', $data[0]); - - //Could be set in other fetchmodes - self::assertArrayNotHasKey(0, $data[0]); - self::assertEquals(1, $data[0]['num']); - } - - public function testQueryOnSlave(): void - { - $conn = $this->createMasterSlaveConnection(); - $conn->connect('slave'); - - $query = 'SELECT count(*) as num FROM master_slave_table'; - - $statement = $conn->query($query); - - self::assertInstanceOf(Statement::class, $statement); - - //Query must be executed only on Master, even when we connect to the slave - self::assertTrue($conn->isConnectedToMaster()); - - $data = $statement->fetchAll(); - - //Default fetchmode is FetchMode::ASSOCIATIVE - self::assertArrayHasKey(0, $data); - self::assertArrayHasKey('num', $data[0]); - - //Could be set in other fetchmodes - self::assertArrayNotHasKey(0, $data[0]); - - self::assertEquals(1, $data[0]['num']); - } -} diff --git a/tests/Doctrine/Tests/DBAL/Functional/PrimaryReplicaconnectionTest.php b/tests/Doctrine/Tests/DBAL/Functional/PrimaryReplicaconnectionTest.php new file mode 100644 index 00000000000..1585d977a63 --- /dev/null +++ b/tests/Doctrine/Tests/DBAL/Functional/PrimaryReplicaconnectionTest.php @@ -0,0 +1,241 @@ +connection->getDatabasePlatform()->getName(); + + // This is a MySQL specific test, skip other vendors. + if ($platformName !== 'mysql') { + $this->markTestSkipped(sprintf('Test does not work on %s.', $platformName)); + } + + try { + $table = new Table('primary_replica_table'); + $table->addColumn('test_int', 'integer'); + $table->setPrimaryKey(['test_int']); + + $sm = $this->connection->getSchemaManager(); + $sm->createTable($table); + } catch (Throwable $e) { + } + + $this->connection->executeUpdate('DELETE FROM primary_replica_table'); + $this->connection->insert('primary_replica_table', ['test_int' => 1]); + } + + private function createPrimaryReplicaConnection(bool $keepReplica = false) : PrimaryReplicaConnection + { + return DriverManager::getConnection($this->createPrimaryReplicaConnectionParams($keepReplica)); + } + + /** + * @return mixed[] + */ + private function createPrimaryReplicaConnectionParams(bool $keepReplica = false) : array + { + $params = $this->connection->getParams(); + $params['primary'] = $params; + $params['replica'] = [$params, $params]; + $params['keepReplica'] = $keepReplica; + $params['wrapperClass'] = PrimaryReplicaConnection::class; + + return $params; + } + + public function testInheritCharsetFromPrimary() : void + { + $charsets = [ + 'utf8', + 'latin1', + ]; + + foreach ($charsets as $charset) { + $params = $this->createPrimaryReplicaConnectionParams(); + $params['primary']['charset'] = $charset; + + foreach ($params['replica'] as $index => $replicaParams) { + if (! isset($replicaParams['charset'])) { + continue; + } + + unset($params['replica'][$index]['charset']); + } + + $conn = DriverManager::getConnection($params); + self::assertInstanceOf(PrimaryReplicaConnection::class, $conn); + $conn->connect('replica'); + + self::assertFalse($conn->isConnectedToPrimary()); + + $clientCharset = $conn->fetchColumn('select @@character_set_client as c'); + + self::assertSame( + $charset, + substr(strtolower($clientCharset), 0, strlen($charset)) + ); + } + } + + public function testPrimaryOnConnect() : void + { + $conn = $this->createPrimaryReplicaConnection(); + + self::assertFalse($conn->isConnectedToPrimary()); + $conn->connect('replica'); + self::assertFalse($conn->isConnectedToPrimary()); + $conn->connect('primary'); + self::assertTrue($conn->isConnectedToPrimary()); + } + + public function testNoPrimaryrOnExecuteQuery() : void + { + $conn = $this->createPrimaryReplicaConnection(); + + $sql = 'SELECT count(*) as num FROM primary_replica_table'; + $data = $conn->fetchAll($sql); + $data[0] = array_change_key_case($data[0], CASE_LOWER); + + self::assertEquals(1, $data[0]['num']); + self::assertFalse($conn->isConnectedToPrimary()); + } + + public function testPrimaryOnWriteOperation() : void + { + $conn = $this->createPrimaryReplicaConnection(); + $conn->insert('primary_replica_table', ['test_int' => 30]); + + self::assertTrue($conn->isConnectedToPrimary()); + + $sql = 'SELECT count(*) as num FROM primary_replica_table'; + $data = $conn->fetchAll($sql); + $data[0] = array_change_key_case($data[0], CASE_LOWER); + + self::assertEquals(2, $data[0]['num']); + self::assertTrue($conn->isConnectedToPrimary()); + } + + /** + * @group DBAL-335 + */ + public function testKeepReplicaBeginTransactionStaysOnPrimary() : void + { + $conn = $this->createPrimaryReplicaConnection($keepReplica = true); + $conn->connect('replica'); + + $conn->beginTransaction(); + $conn->insert('primary_replica_table', ['test_int' => 30]); + $conn->commit(); + + self::assertTrue($conn->isConnectedToPrimary()); + + $conn->connect(); + self::assertTrue($conn->isConnectedToPrimary()); + + $conn->connect('replica'); + self::assertFalse($conn->isConnectedToPrimary()); + } + + /** + * @group DBAL-335 + */ + public function testKeepReplicaInsertStaysOnPrimary() : void + { + $conn = $this->createPrimaryReplicaConnection($keepReplica = true); + $conn->connect('replica'); + + $conn->insert('primary_replica_table', ['test_int' => 30]); + + self::assertTrue($conn->isConnectedToPrimary()); + + $conn->connect(); + self::assertTrue($conn->isConnectedToPrimary()); + + $conn->connect('replica'); + self::assertFalse($conn->isConnectedToPrimary()); + } + + public function testPrimaryReplicaConnectionCloseAndReconnect() : void + { + $conn = $this->createPrimaryReplicaConnection(); + $conn->connect('primary'); + self::assertTrue($conn->isConnectedToPrimary()); + + $conn->close(); + self::assertFalse($conn->isConnectedToPrimary()); + + $conn->connect('primary'); + self::assertTrue($conn->isConnectedToPrimary()); + } + + public function testQueryOnPrimary() : void + { + $conn = $this->createPrimaryReplicaConnection(); + + $query = 'SELECT count(*) as num FROM primary_replica_table'; + + $statement = $conn->query($query); + + self::assertInstanceOf(Statement::class, $statement); + + //Query must be executed only on Primary + self::assertTrue($conn->isConnectedToPrimary()); + + $data = $statement->fetchAll(); + + //Default fetchmode is FetchMode::ASSOCIATIVE + self::assertArrayHasKey(0, $data); + self::assertArrayHasKey('num', $data[0]); + + //Could be set in other fetchmodes + self::assertArrayNotHasKey(0, $data[0]); + self::assertEquals(1, $data[0]['num']); + } + + public function testQueryOnReplica() : void + { + $conn = $this->createPrimaryReplicaConnection(); + $conn->connect('replica'); + + $query = 'SELECT count(*) as num FROM primary_replica_table'; + + $statement = $conn->query($query); + + self::assertInstanceOf(Statement::class, $statement); + + //Query must be executed only on Primary, even when we connect to the replica + self::assertTrue($conn->isConnectedToPrimary()); + + $data = $statement->fetchAll(); + + //Default fetchmode is FetchMode::ASSOCIATIVE + self::assertArrayHasKey(0, $data); + self::assertArrayHasKey('num', $data[0]); + + //Could be set in other fetchmodes + self::assertArrayNotHasKey(0, $data[0]); + + self::assertEquals(1, $data[0]['num']); + } +}