Commit e6a44b14 authored by Benjamin Eberlei's avatar Benjamin Eberlei

[DDC-178] First approach to Locking support

parent 5381e3d5
<?php
/*
* $Id$
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* This software consists of voluntary contributions made by many individuals
* and is licensed under the LGPL. For more information, see
* <http://www.doctrine-project.org>.
*/
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 <kontakt@beberlei.de>
* @author Roman Borschel <roman@code-factory.org>
*/
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
...@@ -488,11 +488,48 @@ abstract class AbstractPlatform ...@@ -488,11 +488,48 @@ abstract class AbstractPlatform
return 'COS(' . $value . ')'; return 'COS(' . $value . ')';
} }
public function getForUpdateSql() public function getForUpdateSQL()
{ {
return 'FOR UPDATE'; 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) public function getDropDatabaseSQL($database)
{ {
return 'DROP DATABASE ' . $database; return 'DROP DATABASE ' . $database;
......
...@@ -483,4 +483,32 @@ class MsSqlPlatform extends AbstractPlatform ...@@ -483,4 +483,32 @@ class MsSqlPlatform extends AbstractPlatform
{ {
return 'TRUNCATE TABLE '.$tableName; 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;
}
}
} }
...@@ -666,4 +666,9 @@ class MySqlPlatform extends AbstractPlatform ...@@ -666,4 +666,9 @@ class MySqlPlatform extends AbstractPlatform
{ {
return true; return true;
} }
public function getReadLockSQL()
{
return 'LOCK IN SHARE MODE';
}
} }
...@@ -637,4 +637,9 @@ class PostgreSqlPlatform extends AbstractPlatform ...@@ -637,4 +637,9 @@ class PostgreSqlPlatform extends AbstractPlatform
{ {
return 'TRUNCATE '.$tableName.' '.($cascade)?'CASCADE':''; return 'TRUNCATE '.$tableName.' '.($cascade)?'CASCADE':'';
} }
public function getReadLockSQL()
{
return 'FOR SHARE';
}
} }
...@@ -428,4 +428,9 @@ class SqlitePlatform extends AbstractPlatform ...@@ -428,4 +428,9 @@ class SqlitePlatform extends AbstractPlatform
} }
return 0; return 0;
} }
public function getForUpdateSql()
{
return '';
}
} }
...@@ -288,11 +288,13 @@ class EntityManager ...@@ -288,11 +288,13 @@ class EntityManager
* *
* @param string $entityName * @param string $entityName
* @param mixed $identifier * @param mixed $identifier
* @param int $lockMode
* @param int $lockVersion
* @return object * @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 @@ class EntityManager ...@@ -447,6 +449,20 @@ class EntityManager
throw new \BadMethodCallException("Not implemented."); 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. * Gets the repository for an entity class.
* *
......
...@@ -92,23 +92,45 @@ class EntityRepository ...@@ -92,23 +92,45 @@ class EntityRepository
* Finds an entity by its primary key / identifier. * Finds an entity by its primary key / identifier.
* *
* @param $id The identifier. * @param $id The identifier.
* @param int $hydrationMode The hydration mode to use. * @param int $lockMode
* @param int $lockVersion
* @return object The entity. * @return object The entity.
*/ */
public function find($id) public function find($id, $lockMode = LockMode::NONE, $lockVersion = null)
{ {
// Check identity map first // Check identity map first
if ($entity = $this->_em->getUnitOfWork()->tryGetById($id, $this->_class->rootEntityName)) { if ($entity = $this->_em->getUnitOfWork()->tryGetById($id, $this->_class->rootEntityName)) {
if ($lockMode != LockMode::NONE) {
$this->_em->lock($entity, $lockMode, $lockVersion);
}
return $entity; // Hit! return $entity; // Hit!
} }
if ( ! is_array($id) || count($id) <= 1) { 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); $value = is_array($id) ? array_values($id) : array($id);
$id = array_combine($this->_class->identifier, $value); $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);
}
} }
/** /**
......
<?php
/*
* $Id$
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* This software consists of voluntary contributions made by many individuals
* and is licensed under the LGPL. For more information, see
* <http://www.doctrine-project.org>.
*/
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 <kontakt@beberlei.de>
* @author Roman Borschel <roman@code-factory.org>
*/
class LockMode extends \Doctrine\DBAL\LockMode
{
}
\ No newline at end of file
...@@ -36,4 +36,9 @@ class OptimisticLockException extends ORMException ...@@ -36,4 +36,9 @@ class OptimisticLockException extends ORMException
{ {
return new self("The optimistic lock failed."); 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
...@@ -235,7 +235,7 @@ class JoinedSubclassPersister extends AbstractEntityInheritancePersister ...@@ -235,7 +235,7 @@ class JoinedSubclassPersister extends AbstractEntityInheritancePersister
/** /**
* {@inheritdoc} * {@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(); $idColumns = $this->_class->getIdentifierColumnNames();
$baseTableAlias = $this->_getSQLTableAlias($this->_class); $baseTableAlias = $this->_getSQLTableAlias($this->_class);
......
...@@ -423,11 +423,12 @@ class StandardEntityPersister ...@@ -423,11 +423,12 @@ class StandardEntityPersister
* a new entity is created. * a new entity is created.
* @param $assoc The association that connects the entity to load to another entity, if any. * @param $assoc The association that connects the entity to load to another entity, if any.
* @param array $hints Hints for entity creation. * @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. * @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); $params = array_values($criteria);
$stmt = $this->_conn->executeQuery($sql, $params); $stmt = $this->_conn->executeQuery($sql, $params);
$result = $stmt->fetch(PDO::FETCH_ASSOC); $result = $stmt->fetch(PDO::FETCH_ASSOC);
...@@ -641,9 +642,10 @@ class StandardEntityPersister ...@@ -641,9 +642,10 @@ class StandardEntityPersister
* @param array $criteria * @param array $criteria
* @param AssociationMapping $assoc * @param AssociationMapping $assoc
* @param string $orderBy * @param string $orderBy
* @param int $lockMode
* @return string * @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 // Construct WHERE conditions
$conditionSql = ''; $conditionSql = '';
...@@ -671,10 +673,17 @@ class StandardEntityPersister ...@@ -671,10 +673,17 @@ class StandardEntityPersister
); );
} }
$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() return 'SELECT ' . $this->_getSelectColumnListSQL()
. ' FROM ' . $this->_class->getQuotedTableName($this->_platform) . ' ' . ' FROM ' . $this->_class->getQuotedTableName($this->_platform) . ' '
. $this->_getSQLTableAlias($this->_class) . $this->_getSQLTableAlias($this->_class)
. ($conditionSql ? ' WHERE ' . $conditionSql : '') . $orderBySql; . ($conditionSql ? ' WHERE ' . $conditionSql : '') . $orderBySql . $lockSql;
} }
/** /**
...@@ -912,4 +921,45 @@ class StandardEntityPersister ...@@ -912,4 +921,45 @@ class StandardEntityPersister
return $tableAlias; 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);
}
} }
<?php
/*
* $Id$
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* This software consists of voluntary contributions made by many individuals
* and is licensed under the LGPL. For more information, see
* <http://www.doctrine-project.org>.
*/
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 <kontakt@beberlei.de>
* @author Roman Borschel <roman@code-factory.org>
*/
class PessimisticLockException extends ORMException
{
public static function lockFailed()
{
return new self("The pessimistic lock failed.");
}
}
\ No newline at end of file
...@@ -97,6 +97,11 @@ final class Query extends AbstractQuery ...@@ -97,6 +97,11 @@ final class Query extends AbstractQuery
*/ */
const HINT_INTERNAL_ITERATION = 'doctrine.internal.iteration'; const HINT_INTERNAL_ITERATION = 'doctrine.internal.iteration';
/**
* @var string
*/
const HINT_LOCK_MODE = 'doctrine.lockMode';
/** /**
* @var integer $_state The current state of this query. * @var integer $_state The current state of this query.
*/ */
...@@ -491,6 +496,39 @@ final class Query extends AbstractQuery ...@@ -491,6 +496,39 @@ final class Query extends AbstractQuery
return parent::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. * Generate a cache id for the query cache - reusing the Result-Cache-Id generator.
* *
......
...@@ -371,6 +371,25 @@ class SqlWalker implements TreeWalker ...@@ -371,6 +371,25 @@ class SqlWalker implements TreeWalker
$sql, $this->_query->getMaxResults(), $this->_query->getFirstResult() $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; return $sql;
} }
...@@ -597,7 +616,7 @@ class SqlWalker implements TreeWalker ...@@ -597,7 +616,7 @@ class SqlWalker implements TreeWalker
$sql .= $this->walkJoinVariableDeclaration($joinVarDecl); $sql .= $this->walkJoinVariableDeclaration($joinVarDecl);
} }
return $sql; return $this->_platform->appendLockHint($sql, $this->_query->getHint(Query::HINT_LOCK_MODE));
} }
/** /**
......
<?php
/*
* $Id$
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* This software consists of voluntary contributions made by many individuals
* and is licensed under the LGPL. For more information, see
* <http://www.doctrine-project.org>.
*/
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 <kontakt@beberlei.de>
* @author Roman Borschel <roman@code-factory.org>
*/
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
...@@ -1631,6 +1631,48 @@ class UnitOfWork implements PropertyChangedListener ...@@ -1631,6 +1631,48 @@ class UnitOfWork implements PropertyChangedListener
} }
} }
/**
* 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. * Gets the CommitOrderCalculator used by the UnitOfWork to order commits.
* *
......
...@@ -93,5 +93,61 @@ class EntityRepositoryTest extends \Doctrine\Tests\OrmFunctionalTestCase ...@@ -93,5 +93,61 @@ class EntityRepositoryTest extends \Doctrine\Tests\OrmFunctionalTestCase
$this->_em->getRepository('Doctrine\Tests\Models\CMS\CmsUser') $this->_em->getRepository('Doctrine\Tests\Models\CMS\CmsUser')
->findByThisFieldDoesNotExist('testvalue'); ->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);
}
} }
***************
*** 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);
+ }
}
...@@ -15,12 +15,15 @@ class SelectSqlGenerationTest extends \Doctrine\Tests\OrmTestCase ...@@ -15,12 +15,15 @@ class SelectSqlGenerationTest extends \Doctrine\Tests\OrmTestCase
$this->_em = $this->_getTestEntityManager(); $this->_em = $this->_getTestEntityManager();
} }
public function assertSqlGeneration($dqlToBeTested, $sqlToBeConfirmed) public function assertSqlGeneration($dqlToBeTested, $sqlToBeConfirmed, array $queryHints = array())
{ {
try { try {
$query = $this->_em->createQuery($dqlToBeTested); $query = $this->_em->createQuery($dqlToBeTested);
$query->setHint(Query::HINT_FORCE_PARTIAL_LOAD, true) $query->setHint(Query::HINT_FORCE_PARTIAL_LOAD, true)
->useQueryCache(false); ->useQueryCache(false);
foreach ($queryHints AS $name => $value) {
$query->setHint($name, $value);
}
parent::assertEquals($sqlToBeConfirmed, $query->getSql()); parent::assertEquals($sqlToBeConfirmed, $query->getSql());
$query->free(); $query->free();
} catch (\Exception $e) { } catch (\Exception $e) {
...@@ -584,4 +587,70 @@ class SelectSqlGenerationTest extends \Doctrine\Tests\OrmTestCase ...@@ -584,4 +587,70 @@ class SelectSqlGenerationTest extends \Doctrine\Tests\OrmTestCase
"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'" "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)
);
}
} }
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