[DDC-178] First approach to Locking support
This commit is contained in:
parent
5381e3d5a4
commit
e6a44b145f
42
lib/Doctrine/DBAL/LockMode.php
Normal file
42
lib/Doctrine/DBAL/LockMode.php
Normal file
@ -0,0 +1,42 @@
|
||||
<?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() { }
|
||||
}
|
@ -488,11 +488,48 @@ abstract class AbstractPlatform
|
||||
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;
|
||||
|
@ -483,4 +483,32 @@ class MsSqlPlatform extends AbstractPlatform
|
||||
{
|
||||
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
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function getReadLockSQL()
|
||||
{
|
||||
return 'LOCK IN SHARE MODE';
|
||||
}
|
||||
}
|
||||
|
@ -637,4 +637,9 @@ class PostgreSqlPlatform extends AbstractPlatform
|
||||
{
|
||||
return 'TRUNCATE '.$tableName.' '.($cascade)?'CASCADE':'';
|
||||
}
|
||||
|
||||
public function getReadLockSQL()
|
||||
{
|
||||
return 'FOR SHARE';
|
||||
}
|
||||
}
|
||||
|
@ -428,4 +428,9 @@ class SqlitePlatform extends AbstractPlatform
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
public function getForUpdateSql()
|
||||
{
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
@ -288,11 +288,13 @@ class EntityManager
|
||||
*
|
||||
* @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 @@ class EntityManager
|
||||
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.
|
||||
*
|
||||
|
@ -92,23 +92,45 @@ class EntityRepository
|
||||
* 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);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
37
lib/Doctrine/ORM/LockMode.php
Normal file
37
lib/Doctrine/ORM/LockMode.php
Normal file
@ -0,0 +1,37 @@
|
||||
<?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
|
||||
{
|
||||
|
||||
}
|
@ -36,4 +36,9 @@ class OptimisticLockException extends ORMException
|
||||
{
|
||||
return new self("The optimistic lock failed.");
|
||||
}
|
||||
|
||||
public static function notVersioned($className)
|
||||
{
|
||||
return new self("Cannot obtain optimistic lock on unversioned entity ".$className);
|
||||
}
|
||||
}
|
@ -235,7 +235,7 @@ class JoinedSubclassPersister extends AbstractEntityInheritancePersister
|
||||
/**
|
||||
* {@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);
|
||||
|
@ -423,11 +423,12 @@ class StandardEntityPersister
|
||||
* 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 @@ class StandardEntityPersister
|
||||
* @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 @@ 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()
|
||||
. ' FROM ' . $this->_class->getQuotedTableName($this->_platform) . ' '
|
||||
. $this->_getSQLTableAlias($this->_class)
|
||||
. ($conditionSql ? ' WHERE ' . $conditionSql : '') . $orderBySql;
|
||||
. ($conditionSql ? ' WHERE ' . $conditionSql : '') . $orderBySql . $lockSql;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -912,4 +921,45 @@ class StandardEntityPersister
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
40
lib/Doctrine/ORM/PessimisticLockException.php
Normal file
40
lib/Doctrine/ORM/PessimisticLockException.php
Normal file
@ -0,0 +1,40 @@
|
||||
<?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.");
|
||||
}
|
||||
}
|
@ -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 @@ final class Query extends AbstractQuery
|
||||
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.
|
||||
*
|
||||
|
@ -371,6 +371,25 @@ class SqlWalker implements TreeWalker
|
||||
$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 @@ class SqlWalker implements TreeWalker
|
||||
$sql .= $this->walkJoinVariableDeclaration($joinVarDecl);
|
||||
}
|
||||
|
||||
return $sql;
|
||||
return $this->_platform->appendLockHint($sql, $this->_query->getHint(Query::HINT_LOCK_MODE));
|
||||
}
|
||||
|
||||
/**
|
||||
|
40
lib/Doctrine/ORM/TransactionRequiredException.php
Normal file
40
lib/Doctrine/ORM/TransactionRequiredException.php
Normal file
@ -0,0 +1,40 @@
|
||||
<?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.');
|
||||
}
|
||||
}
|
@ -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.
|
||||
*
|
||||
|
@ -93,5 +93,61 @@ class EntityRepositoryTest extends \Doctrine\Tests\OrmFunctionalTestCase
|
||||
$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);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
+ }
|
||||
}
|
||||
|
@ -15,12 +15,15 @@ class SelectSqlGenerationTest extends \Doctrine\Tests\OrmTestCase
|
||||
$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 @@ 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'"
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @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)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user