From e6a44b145fbe0ee140fcd1eb9dcf169ab6c9da20 Mon Sep 17 00:00:00 2001 From: Benjamin Eberlei Date: Thu, 8 Apr 2010 22:50:06 +0200 Subject: [PATCH] [DDC-178] First approach to Locking support --- lib/Doctrine/DBAL/LockMode.php | 42 +++++++++++ .../DBAL/Platforms/AbstractPlatform.php | 39 +++++++++- lib/Doctrine/DBAL/Platforms/MsSqlPlatform.php | 28 ++++++++ lib/Doctrine/DBAL/Platforms/MySqlPlatform.php | 5 ++ .../DBAL/Platforms/PostgreSqlPlatform.php | 5 ++ .../DBAL/Platforms/SqlitePlatform.php | 5 ++ lib/Doctrine/ORM/EntityManager.php | 20 +++++- lib/Doctrine/ORM/EntityRepository.php | 30 ++++++-- lib/Doctrine/ORM/LockMode.php | 37 ++++++++++ lib/Doctrine/ORM/OptimisticLockException.php | 5 ++ .../Persisters/JoinedSubclassPersister.php | 2 +- .../Persisters/StandardEntityPersister.php | 58 +++++++++++++-- lib/Doctrine/ORM/PessimisticLockException.php | 40 +++++++++++ lib/Doctrine/ORM/Query.php | 38 ++++++++++ lib/Doctrine/ORM/Query/SqlWalker.php | 21 +++++- .../ORM/TransactionRequiredException.php | 40 +++++++++++ lib/Doctrine/ORM/UnitOfWork.php | 42 +++++++++++ .../ORM/Functional/EntityRepositoryTest.php | 56 +++++++++++++++ .../Functional/EntityRepositoryTest.php.rej | 69 ++++++++++++++++++ .../ORM/Query/SelectSqlGenerationTest.php | 71 ++++++++++++++++++- 20 files changed, 639 insertions(+), 14 deletions(-) create mode 100644 lib/Doctrine/DBAL/LockMode.php create mode 100644 lib/Doctrine/ORM/LockMode.php create mode 100644 lib/Doctrine/ORM/PessimisticLockException.php create mode 100644 lib/Doctrine/ORM/TransactionRequiredException.php create mode 100644 tests/Doctrine/Tests/ORM/Functional/EntityRepositoryTest.php.rej diff --git a/lib/Doctrine/DBAL/LockMode.php b/lib/Doctrine/DBAL/LockMode.php new file mode 100644 index 00000000000..94907216619 --- /dev/null +++ b/lib/Doctrine/DBAL/LockMode.php @@ -0,0 +1,42 @@ +. +*/ + +namespace Doctrine\DBAL; + +/** + * Contains all ORM LockModes + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.com + * @since 1.0 + * @version $Revision$ + * @author Benjamin Eberlei + * @author Roman Borschel + */ +class LockMode +{ + const NONE = 0; + const OPTIMISTIC = 1; + const PESSIMISTIC_READ = 2; + const PESSIMISTIC_WRITE = 4; + + final private function __construct() { } +} \ No newline at end of file diff --git a/lib/Doctrine/DBAL/Platforms/AbstractPlatform.php b/lib/Doctrine/DBAL/Platforms/AbstractPlatform.php index 961ca0b7d44..1ca4c049a1a 100644 --- a/lib/Doctrine/DBAL/Platforms/AbstractPlatform.php +++ b/lib/Doctrine/DBAL/Platforms/AbstractPlatform.php @@ -488,11 +488,48 @@ public function getCosExpression($value) return 'COS(' . $value . ')'; } - public function getForUpdateSql() + public function getForUpdateSQL() { return 'FOR UPDATE'; } + /** + * Honors that some SQL vendors such as MsSql use table hints for locking instead of the ANSI SQL FOR UPDATE specification. + * + * @param string $fromClause + * @param int $lockMode + * @return string + */ + public function appendLockHint($fromClause, $lockMode) + { + return $fromClause; + } + + /** + * Get the sql snippet to append to any SELECT statement which locks rows in shared read lock. + * + * This defaults to the ASNI SQL "FOR UPDATE", which is an exclusive lock (Write). Some database + * vendors allow to lighten this constraint up to be a real read lock. + * + * @return string + */ + public function getReadLockSQL() + { + return $this->getForUpdateSQL(); + } + + /** + * Get the SQL snippet to append to any SELECT statement which obtains an exclusive lock on the rows. + * + * The semantics of this lock mode should equal the SELECT .. FOR UPDATE of the ASNI SQL standard. + * + * @return string + */ + public function getWriteLockSQL() + { + return $this->getForUpdateSQL(); + } + public function getDropDatabaseSQL($database) { return 'DROP DATABASE ' . $database; diff --git a/lib/Doctrine/DBAL/Platforms/MsSqlPlatform.php b/lib/Doctrine/DBAL/Platforms/MsSqlPlatform.php index dd14bd8fd2c..4421c170ce9 100644 --- a/lib/Doctrine/DBAL/Platforms/MsSqlPlatform.php +++ b/lib/Doctrine/DBAL/Platforms/MsSqlPlatform.php @@ -483,4 +483,32 @@ public function getTruncateTableSQL($tableName, $cascade = false) { return 'TRUNCATE TABLE '.$tableName; } + + /** + * MsSql uses Table Hints for locking strategies instead of the ANSI SQL FOR UPDATE like hints. + * + * @return string + */ + public function getForUpdateSQL() + { + return ''; + } + + /** + * @license LGPL + * @author Hibernate + * @param string $fromClause + * @param int $lockMode + * @return string + */ + public function appendLockHint($fromClause, $lockMode) + { + if ($lockMode == \Doctrine\DBAL\LockMode::PESSIMISTIC_WRITE) { + return $fromClause . " WITH (UPDLOCK, ROWLOCK)"; + } else if ( $lockMode == \Doctrine\DBAL\LockMode::PESSIMISTIC_READ ) { + return $fromClause . " WITH (HOLDLOCK, ROWLOCK)"; + } else { + return $fromClause; + } + } } diff --git a/lib/Doctrine/DBAL/Platforms/MySqlPlatform.php b/lib/Doctrine/DBAL/Platforms/MySqlPlatform.php index 5bf9b84c29b..b7ba86d88f4 100644 --- a/lib/Doctrine/DBAL/Platforms/MySqlPlatform.php +++ b/lib/Doctrine/DBAL/Platforms/MySqlPlatform.php @@ -666,4 +666,9 @@ public function createsExplicitIndexForForeignKeys() { return true; } + + public function getReadLockSQL() + { + return 'LOCK IN SHARE MODE'; + } } diff --git a/lib/Doctrine/DBAL/Platforms/PostgreSqlPlatform.php b/lib/Doctrine/DBAL/Platforms/PostgreSqlPlatform.php index 972ddc50a75..f57c8e85adc 100644 --- a/lib/Doctrine/DBAL/Platforms/PostgreSqlPlatform.php +++ b/lib/Doctrine/DBAL/Platforms/PostgreSqlPlatform.php @@ -637,4 +637,9 @@ public function getTruncateTableSQL($tableName, $cascade = false) { return 'TRUNCATE '.$tableName.' '.($cascade)?'CASCADE':''; } + + public function getReadLockSQL() + { + return 'FOR SHARE'; + } } diff --git a/lib/Doctrine/DBAL/Platforms/SqlitePlatform.php b/lib/Doctrine/DBAL/Platforms/SqlitePlatform.php index a1c2184b971..8b209fa6098 100644 --- a/lib/Doctrine/DBAL/Platforms/SqlitePlatform.php +++ b/lib/Doctrine/DBAL/Platforms/SqlitePlatform.php @@ -428,4 +428,9 @@ static public function udfLocate($str, $substr, $offset = 0) } return 0; } + + public function getForUpdateSql() + { + return ''; + } } diff --git a/lib/Doctrine/ORM/EntityManager.php b/lib/Doctrine/ORM/EntityManager.php index 897569b8c51..7cbfb884317 100644 --- a/lib/Doctrine/ORM/EntityManager.php +++ b/lib/Doctrine/ORM/EntityManager.php @@ -288,11 +288,13 @@ public function flush() * * @param string $entityName * @param mixed $identifier + * @param int $lockMode + * @param int $lockVersion * @return object */ - public function find($entityName, $identifier) + public function find($entityName, $identifier, $lockMode = LockMode::NONE, $lockVersion = null) { - return $this->getRepository($entityName)->find($identifier); + return $this->getRepository($entityName)->find($identifier, $lockMode, $lockVersion); } /** @@ -447,6 +449,20 @@ public function copy($entity, $deep = false) throw new \BadMethodCallException("Not implemented."); } + /** + * Acquire a lock on the given entity. + * + * @param object $entity + * @param int $lockMode + * @param int $lockVersion + * @throws OptimisticLockException + * @throws PessimisticLockException + */ + public function lock($entity, $lockMode, $lockVersion) + { + $this->_unitOfWork->lock($entity, $lockMode, $lockVersion); + } + /** * Gets the repository for an entity class. * diff --git a/lib/Doctrine/ORM/EntityRepository.php b/lib/Doctrine/ORM/EntityRepository.php index 1382cb5e6e9..35aa7481e2d 100644 --- a/lib/Doctrine/ORM/EntityRepository.php +++ b/lib/Doctrine/ORM/EntityRepository.php @@ -92,23 +92,45 @@ public function clear() * Finds an entity by its primary key / identifier. * * @param $id The identifier. - * @param int $hydrationMode The hydration mode to use. + * @param int $lockMode + * @param int $lockVersion * @return object The entity. */ - public function find($id) + public function find($id, $lockMode = LockMode::NONE, $lockVersion = null) { // Check identity map first if ($entity = $this->_em->getUnitOfWork()->tryGetById($id, $this->_class->rootEntityName)) { + if ($lockMode != LockMode::NONE) { + $this->_em->lock($entity, $lockMode, $lockVersion); + } + return $entity; // Hit! } if ( ! is_array($id) || count($id) <= 1) { - //FIXME: Not correct. Relies on specific order. + // @todo FIXME: Not correct. Relies on specific order. $value = is_array($id) ? array_values($id) : array($id); $id = array_combine($this->_class->identifier, $value); } - return $this->_em->getUnitOfWork()->getEntityPersister($this->_entityName)->load($id); + if ($lockMode == LockMode::NONE) { + return $this->_em->getUnitOfWork()->getEntityPersister($this->_entityName)->load($id); + } else if ($lockMode == LockMode::OPTIMISTIC) { + if (!$this->_class->isVersioned) { + throw OptimisticLockException::notVersioned($this->_entityName); + } + $entity = $this->_em->getUnitOfWork()->getEntityPersister($this->_entityName)->load($id); + + $this->_em->getUnitOfWork()->lock($entity, $lockMode, $lockVersion); + + return $entity; + } else { + if (!$this->_em->getConnection()->isTransactionActive()) { + throw TransactionRequiredException::transactionRequired(); + } + + return $this->_em->getUnitOfWork()->getEntityPersister($this->_entityName)->load($id, null, null, array(), $lockMode); + } } /** diff --git a/lib/Doctrine/ORM/LockMode.php b/lib/Doctrine/ORM/LockMode.php new file mode 100644 index 00000000000..45f999a3090 --- /dev/null +++ b/lib/Doctrine/ORM/LockMode.php @@ -0,0 +1,37 @@ +. +*/ + +namespace Doctrine\ORM; + +/** + * Contains all ORM LockModes + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.com + * @since 1.0 + * @version $Revision$ + * @author Benjamin Eberlei + * @author Roman Borschel + */ +class LockMode extends \Doctrine\DBAL\LockMode +{ + +} \ No newline at end of file diff --git a/lib/Doctrine/ORM/OptimisticLockException.php b/lib/Doctrine/ORM/OptimisticLockException.php index d937daed435..01f076467ee 100644 --- a/lib/Doctrine/ORM/OptimisticLockException.php +++ b/lib/Doctrine/ORM/OptimisticLockException.php @@ -36,4 +36,9 @@ public static function lockFailed() { return new self("The optimistic lock failed."); } + + public static function notVersioned($className) + { + return new self("Cannot obtain optimistic lock on unversioned entity ".$className); + } } \ No newline at end of file diff --git a/lib/Doctrine/ORM/Persisters/JoinedSubclassPersister.php b/lib/Doctrine/ORM/Persisters/JoinedSubclassPersister.php index c6e08abf49c..ffe96b6cad5 100644 --- a/lib/Doctrine/ORM/Persisters/JoinedSubclassPersister.php +++ b/lib/Doctrine/ORM/Persisters/JoinedSubclassPersister.php @@ -235,7 +235,7 @@ public function delete($entity) /** * {@inheritdoc} */ - protected function _getSelectEntitiesSQL(array &$criteria, $assoc = null, $orderBy = null) + protected function _getSelectEntitiesSQL(array &$criteria, $assoc = null, $orderBy = null, $lockMode = 0) { $idColumns = $this->_class->getIdentifierColumnNames(); $baseTableAlias = $this->_getSQLTableAlias($this->_class); diff --git a/lib/Doctrine/ORM/Persisters/StandardEntityPersister.php b/lib/Doctrine/ORM/Persisters/StandardEntityPersister.php index a02a9b1913a..1bde9a90e48 100644 --- a/lib/Doctrine/ORM/Persisters/StandardEntityPersister.php +++ b/lib/Doctrine/ORM/Persisters/StandardEntityPersister.php @@ -423,11 +423,12 @@ public function getOwningTable($fieldName) * a new entity is created. * @param $assoc The association that connects the entity to load to another entity, if any. * @param array $hints Hints for entity creation. + * @param int $lockMode * @return The loaded entity instance or NULL if the entity/the data can not be found. */ - public function load(array $criteria, $entity = null, $assoc = null, array $hints = array()) + public function load(array $criteria, $entity = null, $assoc = null, array $hints = array(), $lockMode = 0) { - $sql = $this->_getSelectEntitiesSQL($criteria, $assoc); + $sql = $this->_getSelectEntitiesSQL($criteria, $assoc, null, $lockMode); $params = array_values($criteria); $stmt = $this->_conn->executeQuery($sql, $params); $result = $stmt->fetch(PDO::FETCH_ASSOC); @@ -641,9 +642,10 @@ protected function _processSQLResult(array $sqlResult) * @param array $criteria * @param AssociationMapping $assoc * @param string $orderBy + * @param int $lockMode * @return string */ - protected function _getSelectEntitiesSQL(array &$criteria, $assoc = null, $orderBy = null) + protected function _getSelectEntitiesSQL(array &$criteria, $assoc = null, $orderBy = null, $lockMode = 0) { // Construct WHERE conditions $conditionSql = ''; @@ -671,10 +673,17 @@ protected function _getSelectEntitiesSQL(array &$criteria, $assoc = null, $order ); } + $lockSql = ''; + if ($lockMode == \Doctrine\ORM\LockMode::PESSIMISTIC_READ) { + $lockSql = ' ' . $this->_platform->getReadLockSql(); + } else if ($lockMode == \Doctrine\ORM\LockMode::PESSIMISTIC_WRITE) { + $lockSql = ' ' . $this->_platform->getWriteLockSql(); + } + return 'SELECT ' . $this->_getSelectColumnListSQL() . ' FROM ' . $this->_class->getQuotedTableName($this->_platform) . ' ' . $this->_getSQLTableAlias($this->_class) - . ($conditionSql ? ' WHERE ' . $conditionSql : '') . $orderBySql; + . ($conditionSql ? ' WHERE ' . $conditionSql : '') . $orderBySql . $lockSql; } /** @@ -912,4 +921,45 @@ protected function _getSQLTableAlias(ClassMetadata $class) return $tableAlias; } + + /** + * Lock all rows of this entity matching the given criteria with the specified pessimistic lock mode + * + * @param array $criteria + * @param int $lockMode + * @return void + */ + public function lock(array $criteria, $lockMode) + { + // @todo Extract method to remove duplicate code from _getSelectEntitiesSQL()? + $conditionSql = ''; + foreach ($criteria as $field => $value) { + if ($conditionSql != '') { + $conditionSql .= ' AND '; + } + + if (isset($this->_class->columnNames[$field])) { + $conditionSql .= $this->_class->getQuotedColumnName($field, $this->_platform); + } else if (isset($this->_class->fieldNames[$field])) { + $conditionSql .= $this->_class->getQuotedColumnName($this->_class->fieldNames[$field], $this->_platform); + } else if ($assoc !== null) { + $conditionSql .= $field; + } else { + throw ORMException::unrecognizedField($field); + } + $conditionSql .= ' = ?'; + } + + if ($lockMode == \Doctrine\ORM\LockMode::PESSIMISTIC_READ) { + $lockSql = $this->_platform->getReadLockSql(); + } else if ($lockMode == \Doctrine\ORM\LockMode::PESSIMISTIC_WRITE) { + $lockSql = $this->_platform->getWriteLockSql(); + } + + $sql = 'SELECT 1 FROM ' . $this->_class->getQuotedTableName($this->_platform) . ' ' + . $this->_getSQLTableAlias($this->_class) + . ($conditionSql ? ' WHERE ' . $conditionSql : '') . ' ' . $lockSql; + $params = array_values($criteria); + $this->_conn->executeQuery($query, $params); + } } diff --git a/lib/Doctrine/ORM/PessimisticLockException.php b/lib/Doctrine/ORM/PessimisticLockException.php new file mode 100644 index 00000000000..928ead7650e --- /dev/null +++ b/lib/Doctrine/ORM/PessimisticLockException.php @@ -0,0 +1,40 @@ +. +*/ + +namespace Doctrine\ORM; + +/** + * Pessimistic Lock Exception + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.com + * @since 1.0 + * @version $Revision$ + * @author Benjamin Eberlei + * @author Roman Borschel + */ +class PessimisticLockException extends ORMException +{ + public static function lockFailed() + { + return new self("The pessimistic lock failed."); + } +} \ No newline at end of file diff --git a/lib/Doctrine/ORM/Query.php b/lib/Doctrine/ORM/Query.php index 7bbb663f415..bb3ab71f82f 100644 --- a/lib/Doctrine/ORM/Query.php +++ b/lib/Doctrine/ORM/Query.php @@ -97,6 +97,11 @@ final class Query extends AbstractQuery */ const HINT_INTERNAL_ITERATION = 'doctrine.internal.iteration'; + /** + * @var string + */ + const HINT_LOCK_MODE = 'doctrine.lockMode'; + /** * @var integer $_state The current state of this query. */ @@ -491,6 +496,39 @@ public function setHydrationMode($hydrationMode) return parent::setHydrationMode($hydrationMode); } + /** + * Set the lock mode for this Query. + * + * @see Doctrine\ORM\LockMode + * @param int $lockMode + * @return Query + */ + public function setLockMode($lockMode) + { + if ($lockMode == LockMode::PESSIMISTIC_READ || $lockMode == LockMode::PESSIMISTIC_WRITE) { + if (!$this->_em->getConnection()->isTransactionActive()) { + throw TransactionRequiredException::transactionRequired(); + } + } + + $this->setHint(self::HINT_LOCK_MODE, $lockMode); + return $this; + } + + /** + * Get the current lock mode for this query. + * + * @return int + */ + public function getLockMode() + { + $lockMode = $this->getHint(self::HINT_LOCK_MODE); + if (!$lockMode) { + return LockMode::NONE; + } + return $lockMode; + } + /** * Generate a cache id for the query cache - reusing the Result-Cache-Id generator. * diff --git a/lib/Doctrine/ORM/Query/SqlWalker.php b/lib/Doctrine/ORM/Query/SqlWalker.php index d16d9ff60cd..5eff0110f49 100644 --- a/lib/Doctrine/ORM/Query/SqlWalker.php +++ b/lib/Doctrine/ORM/Query/SqlWalker.php @@ -371,6 +371,25 @@ public function walkSelectStatement(AST\SelectStatement $AST) $sql, $this->_query->getMaxResults(), $this->_query->getFirstResult() ); + if (($lockMode = $this->_query->getHint(Query::HINT_LOCK_MODE)) !== false) { + if ($lockMode == \Doctrine\ORM\LockMode::PESSIMISTIC_READ) { + $sql .= " " . $this->_platform->getReadLockSQL(); + } else if ($lockMode == \Doctrine\ORM\LockMode::PESSIMISTIC_WRITE) { + $sql .= " " . $this->_platform->getWriteLockSQL(); + } else if ($lockMode == \Doctrine\ORM\LockMode::OPTIMISTIC) { + $versionedClassFound = false; + foreach ($this->_selectedClasses AS $class) { + if ($class->isVersioned) { + $versionedClassFound = true; + } + } + + if (!$versionedClassFound) { + throw \Doctrine\ORM\OptimisticLockException::lockFailed(); + } + } + } + return $sql; } @@ -597,7 +616,7 @@ public function walkFromClause($fromClause) $sql .= $this->walkJoinVariableDeclaration($joinVarDecl); } - return $sql; + return $this->_platform->appendLockHint($sql, $this->_query->getHint(Query::HINT_LOCK_MODE)); } /** diff --git a/lib/Doctrine/ORM/TransactionRequiredException.php b/lib/Doctrine/ORM/TransactionRequiredException.php new file mode 100644 index 00000000000..170f63e5047 --- /dev/null +++ b/lib/Doctrine/ORM/TransactionRequiredException.php @@ -0,0 +1,40 @@ +. +*/ + +namespace Doctrine\ORM; + +/** + * Is thrown when a transaction is required for the current operation, but there is none open. + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.com + * @since 1.0 + * @version $Revision$ + * @author Benjamin Eberlei + * @author Roman Borschel + */ +class TransactionRequiredException extends ORMException +{ + static public function transactionRequired() + { + return new self('An open transaction is required for this operation.'); + } +} \ No newline at end of file diff --git a/lib/Doctrine/ORM/UnitOfWork.php b/lib/Doctrine/ORM/UnitOfWork.php index d1a89bed418..f084640d876 100644 --- a/lib/Doctrine/ORM/UnitOfWork.php +++ b/lib/Doctrine/ORM/UnitOfWork.php @@ -1631,6 +1631,48 @@ private function _cascadeRemove($entity, array &$visited) } } + /** + * Acquire a lock on the given entity. + * + * @param object $entity + * @param int $lockMode + * @param int $lockVersion + */ + public function lock($entity, $lockMode, $lockVersion = null) + { + $entityName = get_class($entity); + $class = $this->_em->getClassMetadata($entityName); + + if ($lockMode == LockMode::OPTIMISTIC) { + if (!$class->isVersioned) { + throw OptimisticLockException::notVersioned($entityName); + } + + if ($lockVersion != null) { + $entityVersion = $class->reflFields[$class->versionField]->getValue($entity); + if ($entityVersion != $lockVersion) { + throw OptimisticLockException::lockFailed(); + } + } + } else if ($lockMode == LockMode::PESSIMISTIC_READ || $lockMode == LockMode::PESSIMISTIC_WRITE) { + + if (!$this->_em->getConnection()->isTransactionActive()) { + throw TransactionRequiredException::transactionRequired(); + } + + if ($this->getEntityState($entity) == self::STATE_MANAGED) { + $oid = spl_object_hash($entity); + + $this->getEntityPersister($class->name)->lock( + array_combine($class->getIdentifierColumnNames(), $this->_entityIdentifiers[$oid]), + $entity + ); + } else { + throw new \InvalidArgumentException("Entity is not MANAGED."); + } + } + } + /** * Gets the CommitOrderCalculator used by the UnitOfWork to order commits. * diff --git a/tests/Doctrine/Tests/ORM/Functional/EntityRepositoryTest.php b/tests/Doctrine/Tests/ORM/Functional/EntityRepositoryTest.php index 856c7b8d22d..7007c5e2657 100644 --- a/tests/Doctrine/Tests/ORM/Functional/EntityRepositoryTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/EntityRepositoryTest.php @@ -93,5 +93,61 @@ public function testExceptionIsThrownWhenUsingInvalidFieldName() { $this->_em->getRepository('Doctrine\Tests\Models\CMS\CmsUser') ->findByThisFieldDoesNotExist('testvalue'); } + + /** + * @group locking + * @group DDC-178 + */ + public function testPessimisticReadLockWithoutTransaction_ThrowsException() + { + $this->setExpectedException('Doctrine\ORM\TransactionRequiredException'); + + $this->_em->getRepository('Doctrine\Tests\Models\CMS\CmsUser') + ->find(1, \Doctrine\ORM\LockMode::PESSIMISTIC_READ); + } + + /** + * @group locking + * @group DDC-178 + */ + public function testPessimisticWriteLockWithoutTransaction_ThrowsException() + { + $this->setExpectedException('Doctrine\ORM\TransactionRequiredException'); + + $this->_em->getRepository('Doctrine\Tests\Models\CMS\CmsUser') + ->find(1, \Doctrine\ORM\LockMode::PESSIMISTIC_WRITE); + } + + /** + * @group locking + * @group DDC-178 + */ + public function testOptimisticLockUnversionedEntity_ThrowsException() + { + $this->setExpectedException('Doctrine\ORM\OptimisticLockException'); + + $this->_em->getRepository('Doctrine\Tests\Models\CMS\CmsUser') + ->find(1, \Doctrine\ORM\LockMode::OPTIMISTIC); + } + + /** + * @group locking + * @group DDC-178 + */ + public function testIdentityMappedOptimisticLockUnversionedEntity_ThrowsException() + { + $user = new CmsUser; + $user->name = 'Roman'; + $user->username = 'romanb'; + $user->status = 'freak'; + $this->_em->persist($user); + $this->_em->flush(); + + $userId = $user->id; + + $this->setExpectedException('Doctrine\ORM\OptimisticLockException'); + $this->_em->find('Doctrine\Tests\Models\Cms\CmsUser', $userId); + $this->_em->find('Doctrine\Tests\Models\Cms\CmsUser', $userId, \Doctrine\ORM\LockMode::OPTIMISTIC); + } } diff --git a/tests/Doctrine/Tests/ORM/Functional/EntityRepositoryTest.php.rej b/tests/Doctrine/Tests/ORM/Functional/EntityRepositoryTest.php.rej new file mode 100644 index 00000000000..17f059d65da --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Functional/EntityRepositoryTest.php.rej @@ -0,0 +1,69 @@ +*************** +*** 93,97 **** + $this->_em->getRepository('Doctrine\Tests\Models\CMS\CmsUser') + ->findByThisFieldDoesNotExist('testvalue'); + } + } + +--- 93,153 ---- + $this->_em->getRepository('Doctrine\Tests\Models\CMS\CmsUser') + ->findByThisFieldDoesNotExist('testvalue'); + } ++ ++ /** ++ * @group locking ++ * @group DDC-178 ++ */ ++ public function testPessimisticReadLockWithoutTransaction_ThrowsException() ++ { ++ $this->setExpectedException('Doctrine\ORM\TransactionRequiredException'); ++ ++ $this->_em->getRepository('Doctrine\Tests\Models\CMS\CmsUser') ++ ->find(1, \Doctrine\ORM\LockMode::PESSIMISTIC_READ); ++ } ++ ++ /** ++ * @group locking ++ * @group DDC-178 ++ */ ++ public function testPessimisticWriteLockWithoutTransaction_ThrowsException() ++ { ++ $this->setExpectedException('Doctrine\ORM\TransactionRequiredException'); ++ ++ $this->_em->getRepository('Doctrine\Tests\Models\CMS\CmsUser') ++ ->find(1, \Doctrine\ORM\LockMode::PESSIMISTIC_WRITE); ++ } ++ ++ /** ++ * @group locking ++ * @group DDC-178 ++ */ ++ public function testOptimisticLockUnversionedEntity_ThrowsException() ++ { ++ $this->setExpectedException('Doctrine\ORM\OptimisticLockException'); ++ ++ $this->_em->getRepository('Doctrine\Tests\Models\CMS\CmsUser') ++ ->find(1, \Doctrine\ORM\LockMode::OPTIMISTIC); ++ } ++ ++ /** ++ * @group locking ++ * @group DDC-178 ++ */ ++ public function testIdentityMappedOptimisticLockUnversionedEntity_ThrowsException() ++ { ++ $user = new CmsUser; ++ $user->name = 'Roman'; ++ $user->username = 'romanb'; ++ $user->status = 'freak'; ++ $this->_em->persist($user); ++ $this->_em->flush(); ++ ++ $userId = $user->id; ++ ++ $this->setExpectedException('Doctrine\ORM\OptimisticLockException'); ++ $this->_em->find('Doctrine\Tests\Models\Cms\CmsUser', $userId); ++ $this->_em->find('Doctrine\Tests\Models\Cms\CmsUser', $userId, \Doctrine\ORM\LockMode::OPTIMISTIC); ++ } + } + diff --git a/tests/Doctrine/Tests/ORM/Query/SelectSqlGenerationTest.php b/tests/Doctrine/Tests/ORM/Query/SelectSqlGenerationTest.php index 163bc594ece..812513cd688 100644 --- a/tests/Doctrine/Tests/ORM/Query/SelectSqlGenerationTest.php +++ b/tests/Doctrine/Tests/ORM/Query/SelectSqlGenerationTest.php @@ -15,12 +15,15 @@ protected function setUp() $this->_em = $this->_getTestEntityManager(); } - public function assertSqlGeneration($dqlToBeTested, $sqlToBeConfirmed) + public function assertSqlGeneration($dqlToBeTested, $sqlToBeConfirmed, array $queryHints = array()) { try { $query = $this->_em->createQuery($dqlToBeTested); $query->setHint(Query::HINT_FORCE_PARTIAL_LOAD, true) ->useQueryCache(false); + foreach ($queryHints AS $name => $value) { + $query->setHint($name, $value); + } parent::assertEquals($sqlToBeConfirmed, $query->getSql()); $query->free(); } catch (\Exception $e) { @@ -584,4 +587,70 @@ public function testSubselectInSelect() "SELECT c0_.name AS name0, (SELECT COUNT(c1_.phonenumber) AS dctrn__1 FROM cms_phonenumbers c1_ WHERE c1_.phonenumber = 1234) AS sclr1 FROM cms_users c0_ WHERE c0_.name = 'jon'" ); } + + /** + * @group locking + * @group DDC-178 + */ + public function testPessimisticWriteLockQueryHint() + { + if ($this->_em->getConnection()->getDatabasePlatform() instanceof \Doctrine\DBAL\Platforms\SqlitePlatform) { + $this->markTestSkipped('SqLite does not support Row locking at all.'); + } + + $this->assertSqlGeneration( + "SELECT u FROM Doctrine\Tests\Models\CMS\CmsUser u WHERE u.username = 'gblanco'", + "SELECT c0_.id AS id0, c0_.status AS status1, c0_.username AS username2, c0_.name AS name3 ". + "FROM cms_users c0_ WHERE c0_.username = 'gblanco' FOR UPDATE", + array(Query::HINT_LOCK_MODE => \Doctrine\ORM\LockMode::PESSIMISTIC_WRITE) + ); + } + + /** + * @group locking + * @group DDC-178 + */ + public function testPessimisticReadLockQueryHintPostgreSql() + { + $this->_em->getConnection()->setDatabasePlatform(new \Doctrine\DBAL\Platforms\PostgreSqlPlatform); + + $this->assertSqlGeneration( + "SELECT u FROM Doctrine\Tests\Models\CMS\CmsUser u WHERE u.username = 'gblanco'", + "SELECT c0_.id AS id0, c0_.status AS status1, c0_.username AS username2, c0_.name AS name3 ". + "FROM cms_users c0_ WHERE c0_.username = 'gblanco' FOR SHARE", + array(Query::HINT_LOCK_MODE => \Doctrine\ORM\LockMode::PESSIMISTIC_READ) + ); + } + + /** + * @group locking + * @group DDC-178 + */ + public function testPessimisticReadLockQueryHintMySql() + { + $this->_em->getConnection()->setDatabasePlatform(new \Doctrine\DBAL\Platforms\MySqlPlatform); + + $this->assertSqlGeneration( + "SELECT u FROM Doctrine\Tests\Models\CMS\CmsUser u WHERE u.username = 'gblanco'", + "SELECT c0_.id AS id0, c0_.status AS status1, c0_.username AS username2, c0_.name AS name3 ". + "FROM cms_users c0_ WHERE c0_.username = 'gblanco' LOCK IN SHARE MODE", + array(Query::HINT_LOCK_MODE => \Doctrine\ORM\LockMode::PESSIMISTIC_READ) + ); + } + + /** + * @group locking + * @group DDC-178 + */ + public function testPessimisticReadLockQueryHintOracle() + { + $this->_em->getConnection()->setDatabasePlatform(new \Doctrine\DBAL\Platforms\OraclePlatform); + + $this->assertSqlGeneration( + "SELECT u FROM Doctrine\Tests\Models\CMS\CmsUser u WHERE u.username = 'gblanco'", + "SELECT c0_.id AS id0, c0_.status AS status1, c0_.username AS username2, c0_.name AS name3 ". + "FROM cms_users c0_ WHERE c0_.username = 'gblanco' FOR UPDATE", + array(Query::HINT_LOCK_MODE => \Doctrine\ORM\LockMode::PESSIMISTIC_READ) + ); + } }