Merge branch 'master' into develop
This commit is contained in:
commit
e958396450
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;
|
||||
|
@ -513,4 +513,9 @@ class DB2Platform extends AbstractPlatform
|
||||
{
|
||||
return strtoupper($column);
|
||||
}
|
||||
|
||||
public function getForUpdateSQL()
|
||||
{
|
||||
return ' WITH RR USE AND KEEP UPDATE LOCKS';
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -583,4 +583,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 '';
|
||||
}
|
||||
}
|
||||
|
@ -318,11 +318,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);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -478,6 +480,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 = null)
|
||||
{
|
||||
$this->_unitOfWork->lock($entity, $lockMode, $lockVersion);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the repository for an entity class.
|
||||
*
|
||||
|
@ -87,23 +87,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);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
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
|
||||
{
|
||||
|
||||
}
|
@ -24,6 +24,7 @@ namespace Doctrine\ORM;
|
||||
* that uses optimistic locking through a version field fails.
|
||||
*
|
||||
* @author Roman Borschel <roman@code-factory.org>
|
||||
* @author Benjamin Eberlei <kontakt@beberlei.de>
|
||||
* @since 2.0
|
||||
*/
|
||||
class OptimisticLockException extends ORMException
|
||||
@ -49,4 +50,14 @@ class OptimisticLockException extends ORMException
|
||||
{
|
||||
return new self("The optimistic lock on an entity failed.", $entity);
|
||||
}
|
||||
|
||||
public static function lockFailedVersionMissmatch($entity, $expectedLockVersion, $actualLockVersion)
|
||||
{
|
||||
return new self("The optimistic lock failed, version " . $expectedLockVersion . " was expected, but is actually ".$actualLockVersion, $entity);
|
||||
}
|
||||
|
||||
public static function notVersioned($entityName)
|
||||
{
|
||||
return new self("Cannot obtain optimistic lock on unversioned entity " . $entityName, null);
|
||||
}
|
||||
}
|
@ -482,12 +482,13 @@ class BasicEntityPersister
|
||||
* 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 object The loaded and managed entity instance or NULL if the entity can not be found.
|
||||
* @todo Check identity map? loadById method? Try to guess whether $criteria is the id?
|
||||
*/
|
||||
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, $lockMode);
|
||||
$stmt = $this->_conn->executeQuery($sql, array_values($criteria));
|
||||
$result = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
$stmt->closeCursor();
|
||||
@ -772,10 +773,12 @@ class BasicEntityPersister
|
||||
*
|
||||
* @param array $criteria
|
||||
* @param AssociationMapping $assoc
|
||||
* @param string $orderBy
|
||||
* @param int $lockMode
|
||||
* @return string
|
||||
* @todo Refactor: _getSelectSQL(...)
|
||||
*/
|
||||
protected function _getSelectEntitiesSQL(array $criteria, $assoc = null)
|
||||
protected function _getSelectEntitiesSQL(array $criteria, $assoc = null, $lockMode = 0)
|
||||
{
|
||||
$joinSql = $assoc != null && $assoc->isManyToMany() ?
|
||||
$this->_getSelectManyToManyJoinSQL($assoc) : '';
|
||||
@ -786,12 +789,20 @@ class BasicEntityPersister
|
||||
$this->_getCollectionOrderBySQL($assoc->orderBy, $this->_getSQLTableAlias($this->_class->name))
|
||||
: '';
|
||||
|
||||
$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->name)
|
||||
. $joinSql
|
||||
. ($conditionSql ? ' WHERE ' . $conditionSql : '')
|
||||
. $orderBySql;
|
||||
. $orderBySql
|
||||
. $lockSql;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -1006,6 +1017,30 @@ class BasicEntityPersister
|
||||
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)
|
||||
{
|
||||
$conditionSql = $this->_getSelectConditionSQL($criteria);
|
||||
|
||||
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->name)
|
||||
. ($conditionSql ? ' WHERE ' . $conditionSql : '') . ' ' . $lockSql;
|
||||
$params = array_values($criteria);
|
||||
$this->_conn->executeQuery($sql, $params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the conditional SQL fragment used in the WHERE clause when selecting
|
||||
* entities in this persister.
|
||||
@ -1043,7 +1078,6 @@ class BasicEntityPersister
|
||||
}
|
||||
$conditionSql .= ' = ?';
|
||||
}
|
||||
|
||||
return $conditionSql;
|
||||
}
|
||||
|
||||
|
@ -228,7 +228,7 @@ class JoinedSubclassPersister extends AbstractEntityInheritancePersister
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function _getSelectEntitiesSQL(array $criteria, $assoc = null)
|
||||
protected function _getSelectEntitiesSQL(array $criteria, $assoc = null, $lockMode = 0)
|
||||
{
|
||||
$idColumns = $this->_class->getIdentifierColumnNames();
|
||||
$baseTableAlias = $this->_getSQLTableAlias($this->_class->name);
|
||||
@ -345,6 +345,18 @@ class JoinedSubclassPersister extends AbstractEntityInheritancePersister
|
||||
. $joinSql
|
||||
. ($conditionSql != '' ? ' WHERE ' . $conditionSql : '') . $orderBySql;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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)
|
||||
{
|
||||
throw new \BadMethodCallException("lock() is not yet supported for JoinedSubclassPersister");
|
||||
}
|
||||
|
||||
/* Ensure this method is never called. This persister overrides _getSelectEntitiesSQL directly. */
|
||||
protected function _getSelectColumnListSQL()
|
||||
|
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.");
|
||||
}
|
||||
}
|
@ -93,6 +93,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.
|
||||
*/
|
||||
@ -487,6 +492,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.
|
||||
*
|
||||
|
@ -366,6 +366,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;
|
||||
}
|
||||
|
||||
@ -603,7 +622,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.');
|
||||
}
|
||||
}
|
@ -1346,7 +1346,7 @@ class UnitOfWork implements PropertyChangedListener
|
||||
$entityVersion = $class->reflFields[$class->versionField]->getValue($entity);
|
||||
// Throw exception if versions dont match.
|
||||
if ($managedCopyVersion != $entityVersion) {
|
||||
throw OptimisticLockException::lockFailed($entity);
|
||||
throw OptimisticLockException::lockFailedVersionMissmatch($entityVersion, $managedCopyVersion);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1630,6 +1630,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)
|
||||
{
|
||||
if ($this->getEntityState($entity) != self::STATE_MANAGED) {
|
||||
throw new \InvalidArgumentException("Entity is not MANAGED.");
|
||||
}
|
||||
|
||||
$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::lockFailedVersionMissmatch($entity, $lockVersion, $entityVersion);
|
||||
}
|
||||
}
|
||||
} else if ($lockMode == LockMode::PESSIMISTIC_READ || $lockMode == LockMode::PESSIMISTIC_WRITE) {
|
||||
|
||||
if (!$this->_em->getConnection()->isTransactionActive()) {
|
||||
throw TransactionRequiredException::transactionRequired();
|
||||
}
|
||||
|
||||
$oid = spl_object_hash($entity);
|
||||
|
||||
$this->getEntityPersister($class->name)->lock(
|
||||
array_combine($class->getIdentifierColumnNames(), $this->_entityIdentifiers[$oid]),
|
||||
$lockMode
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the CommitOrderCalculator used by the UnitOfWork to order commits.
|
||||
*
|
||||
|
@ -31,6 +31,11 @@ class CmsArticle
|
||||
* @OneToMany(targetEntity="CmsComment", mappedBy="article")
|
||||
*/
|
||||
public $comments;
|
||||
|
||||
/**
|
||||
* @Version @column(type="integer")
|
||||
*/
|
||||
public $version;
|
||||
|
||||
public function setAuthor(CmsUser $author) {
|
||||
$this->user = $author;
|
||||
|
@ -93,5 +93,62 @@ 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->_em->find('Doctrine\Tests\Models\Cms\CmsUser', $userId);
|
||||
|
||||
$this->setExpectedException('Doctrine\ORM\OptimisticLockException');
|
||||
$this->_em->find('Doctrine\Tests\Models\Cms\CmsUser', $userId, \Doctrine\ORM\LockMode::OPTIMISTIC);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -20,6 +20,7 @@ class AllTests
|
||||
$suite = new \Doctrine\Tests\DoctrineTestSuite('Doctrine Orm Functional Locking');
|
||||
|
||||
$suite->addTestSuite('Doctrine\Tests\ORM\Functional\Locking\OptimisticTest');
|
||||
$suite->addTestSuite('Doctrine\Tests\ORM\Functional\Locking\LockTest');
|
||||
|
||||
return $suite;
|
||||
}
|
||||
|
177
tests/Doctrine/Tests/ORM/Functional/Locking/GearmanLockTest.php
Normal file
177
tests/Doctrine/Tests/ORM/Functional/Locking/GearmanLockTest.php
Normal file
@ -0,0 +1,177 @@
|
||||
<?php
|
||||
|
||||
namespace Doctrine\Tests\ORM\Functional\Locking;
|
||||
|
||||
use Doctrine\Tests\Models\CMS\CmsArticle,
|
||||
Doctrine\Tests\Models\CMS\CmsUser,
|
||||
Doctrine\ORM\LockMode,
|
||||
Doctrine\ORM\EntityManager;
|
||||
|
||||
require_once __DIR__ . '/../../../TestInit.php';
|
||||
|
||||
class GearmanLockTest extends \Doctrine\Tests\OrmFunctionalTestCase
|
||||
{
|
||||
private $gearman = null;
|
||||
private $maxRunTime = 0;
|
||||
private $articleId;
|
||||
|
||||
protected function setUp()
|
||||
{
|
||||
if (!class_exists('GearmanClient', false)) {
|
||||
$this->markTestSkipped('pecl/gearman is required for this test to run.');
|
||||
}
|
||||
|
||||
$this->useModelSet('cms');
|
||||
parent::setUp();
|
||||
$this->tasks = array();
|
||||
|
||||
$this->gearman = new \GearmanClient();
|
||||
$this->gearman->addServer();
|
||||
$this->gearman->setCompleteCallback(array($this, "gearmanTaskCompleted"));
|
||||
|
||||
$article = new CmsArticle();
|
||||
$article->text = "my article";
|
||||
$article->topic = "Hello";
|
||||
|
||||
$this->_em->persist($article);
|
||||
$this->_em->flush();
|
||||
|
||||
$this->articleId = $article->id;
|
||||
}
|
||||
|
||||
public function gearmanTaskCompleted($task)
|
||||
{
|
||||
$this->maxRunTime = max($this->maxRunTime, $task->data());
|
||||
}
|
||||
|
||||
public function testFindWithLock()
|
||||
{
|
||||
$this->asyncFindWithLock('Doctrine\Tests\Models\CMS\CmsArticle', $this->articleId, LockMode::PESSIMISTIC_WRITE);
|
||||
$this->asyncFindWithLock('Doctrine\Tests\Models\CMS\CmsArticle', $this->articleId, LockMode::PESSIMISTIC_WRITE);
|
||||
|
||||
$this->assertLockWorked();
|
||||
}
|
||||
|
||||
public function testFindWithWriteThenReadLock()
|
||||
{
|
||||
$this->asyncFindWithLock('Doctrine\Tests\Models\CMS\CmsArticle', $this->articleId, LockMode::PESSIMISTIC_WRITE);
|
||||
$this->asyncFindWithLock('Doctrine\Tests\Models\CMS\CmsArticle', $this->articleId, LockMode::PESSIMISTIC_READ);
|
||||
|
||||
$this->assertLockWorked();
|
||||
}
|
||||
|
||||
public function testFindWithReadThenWriteLock()
|
||||
{
|
||||
$this->asyncFindWithLock('Doctrine\Tests\Models\CMS\CmsArticle', $this->articleId, LockMode::PESSIMISTIC_READ);
|
||||
$this->asyncFindWithLock('Doctrine\Tests\Models\CMS\CmsArticle', $this->articleId, LockMode::PESSIMISTIC_WRITE);
|
||||
|
||||
$this->assertLockWorked();
|
||||
}
|
||||
|
||||
public function testFindWithOneLock()
|
||||
{
|
||||
$this->asyncFindWithLock('Doctrine\Tests\Models\CMS\CmsArticle', $this->articleId, LockMode::PESSIMISTIC_WRITE);
|
||||
$this->asyncFindWithLock('Doctrine\Tests\Models\CMS\CmsArticle', $this->articleId, LockMode::NONE);
|
||||
|
||||
$this->assertLockDoesNotBlock();
|
||||
}
|
||||
|
||||
public function testDqlWithLock()
|
||||
{
|
||||
$this->asyncDqlWithLock('SELECT a FROM Doctrine\Tests\Models\CMS\CmsArticle a', array(), LockMode::PESSIMISTIC_WRITE);
|
||||
$this->asyncFindWithLock('Doctrine\Tests\Models\CMS\CmsArticle', $this->articleId, LockMode::PESSIMISTIC_WRITE);
|
||||
|
||||
$this->assertLockWorked();
|
||||
}
|
||||
|
||||
public function testLock()
|
||||
{
|
||||
$this->asyncFindWithLock('Doctrine\Tests\Models\CMS\CmsArticle', $this->articleId, LockMode::PESSIMISTIC_WRITE);
|
||||
$this->asyncLock('Doctrine\Tests\Models\CMS\CmsArticle', $this->articleId, LockMode::PESSIMISTIC_WRITE);
|
||||
|
||||
$this->assertLockWorked();
|
||||
}
|
||||
|
||||
public function testLock2()
|
||||
{
|
||||
$this->asyncFindWithLock('Doctrine\Tests\Models\CMS\CmsArticle', $this->articleId, LockMode::PESSIMISTIC_WRITE);
|
||||
$this->asyncLock('Doctrine\Tests\Models\CMS\CmsArticle', $this->articleId, LockMode::PESSIMISTIC_READ);
|
||||
|
||||
$this->assertLockWorked();
|
||||
}
|
||||
|
||||
public function testLock3()
|
||||
{
|
||||
$this->asyncFindWithLock('Doctrine\Tests\Models\CMS\CmsArticle', $this->articleId, LockMode::PESSIMISTIC_READ);
|
||||
$this->asyncLock('Doctrine\Tests\Models\CMS\CmsArticle', $this->articleId, LockMode::PESSIMISTIC_WRITE);
|
||||
|
||||
$this->assertLockWorked();
|
||||
}
|
||||
|
||||
public function testLock4()
|
||||
{
|
||||
$this->asyncFindWithLock('Doctrine\Tests\Models\CMS\CmsArticle', $this->articleId, LockMode::NONE);
|
||||
$this->asyncLock('Doctrine\Tests\Models\CMS\CmsArticle', $this->articleId, LockMode::PESSIMISTIC_WRITE);
|
||||
|
||||
$this->assertLockDoesNotBlock();
|
||||
}
|
||||
|
||||
protected function assertLockDoesNotBlock()
|
||||
{
|
||||
$this->assertLockWorked($onlyForSeconds = 1);
|
||||
}
|
||||
|
||||
protected function assertLockWorked($forTime = 2, $notLongerThan = null)
|
||||
{
|
||||
if ($notLongerThan === null) {
|
||||
$notLongerThan = $forTime + 1;
|
||||
}
|
||||
|
||||
$this->gearman->runTasks();
|
||||
|
||||
$this->assertTrue($this->maxRunTime > $forTime,
|
||||
"Because of locking this tests should have run at least " . $forTime . " seconds, ".
|
||||
"but only did for " . $this->maxRunTime . " seconds.");
|
||||
$this->assertTrue($this->maxRunTime < $notLongerThan,
|
||||
"The longest task should not run longer than " . $notLongerThan . " seconds, ".
|
||||
"but did for " . $this->maxRunTime . " seconds."
|
||||
);
|
||||
}
|
||||
|
||||
protected function asyncFindWithLock($entityName, $entityId, $lockMode)
|
||||
{
|
||||
$this->startJob('findWithLock', array(
|
||||
'entityName' => $entityName,
|
||||
'entityId' => $entityId,
|
||||
'lockMode' => $lockMode,
|
||||
));
|
||||
}
|
||||
|
||||
protected function asyncDqlWithLock($dql, $params, $lockMode)
|
||||
{
|
||||
$this->startJob('dqlWithLock', array(
|
||||
'dql' => $dql,
|
||||
'dqlParams' => $params,
|
||||
'lockMode' => $lockMode,
|
||||
));
|
||||
}
|
||||
|
||||
protected function asyncLock($entityName, $entityId, $lockMode)
|
||||
{
|
||||
$this->startJob('lock', array(
|
||||
'entityName' => $entityName,
|
||||
'entityId' => $entityId,
|
||||
'lockMode' => $lockMode,
|
||||
));
|
||||
}
|
||||
|
||||
protected function startJob($fn, $fixture)
|
||||
{
|
||||
$this->gearman->addTask($fn, serialize(array(
|
||||
'conn' => $this->_em->getConnection()->getParams(),
|
||||
'fixture' => $fixture
|
||||
)));
|
||||
|
||||
$this->assertEquals(GEARMAN_SUCCESS, $this->gearman->returnCode());
|
||||
}
|
||||
}
|
112
tests/Doctrine/Tests/ORM/Functional/Locking/LockAgentWorker.php
Normal file
112
tests/Doctrine/Tests/ORM/Functional/Locking/LockAgentWorker.php
Normal file
@ -0,0 +1,112 @@
|
||||
<?php
|
||||
|
||||
namespace Doctrine\Tests\ORM\Functional\Locking;
|
||||
|
||||
require_once __DIR__ . "/../../../TestInit.php";
|
||||
|
||||
class LockAgentWorker
|
||||
{
|
||||
private $em;
|
||||
|
||||
static public function run()
|
||||
{
|
||||
$lockAgent = new LockAgentWorker();
|
||||
|
||||
$worker = new \GearmanWorker();
|
||||
$worker->addServer();
|
||||
$worker->addFunction("findWithLock", array($lockAgent, "findWithLock"));
|
||||
$worker->addFunction("dqlWithLock", array($lockAgent, "dqlWithLock"));
|
||||
$worker->addFunction('lock', array($lockAgent, 'lock'));
|
||||
|
||||
while($worker->work()) {
|
||||
if ($worker->returnCode() != GEARMAN_SUCCESS) {
|
||||
echo "return_code: " . $worker->returnCode() . "\n";
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected function process($job, \Closure $do)
|
||||
{
|
||||
$fixture = $this->processWorkload($job);
|
||||
|
||||
$s = microtime(true);
|
||||
$this->em->beginTransaction();
|
||||
$do($fixture, $this->em);
|
||||
|
||||
sleep(1);
|
||||
$this->em->rollback();
|
||||
$this->em->clear();
|
||||
$this->em->close();
|
||||
$this->em->getConnection()->close();
|
||||
|
||||
return (microtime(true) - $s);
|
||||
}
|
||||
|
||||
public function findWithLock($job)
|
||||
{
|
||||
return $this->process($job, function($fixture, $em) {
|
||||
$entity = $em->find($fixture['entityName'], $fixture['entityId'], $fixture['lockMode']);
|
||||
});
|
||||
}
|
||||
|
||||
public function dqlWithLock($job)
|
||||
{
|
||||
return $this->process($job, function($fixture, $em) {
|
||||
/* @var $query Doctrine\ORM\Query */
|
||||
$query = $em->createQuery($fixture['dql']);
|
||||
$query->setLockMode($fixture['lockMode']);
|
||||
$query->setParameters($fixture['dqlParams']);
|
||||
$result = $query->getResult();
|
||||
});
|
||||
}
|
||||
|
||||
public function lock($job)
|
||||
{
|
||||
return $this->process($job, function($fixture, $em) {
|
||||
$entity = $em->find($fixture['entityName'], $fixture['entityId']);
|
||||
$em->lock($entity, $fixture['lockMode']);
|
||||
});
|
||||
}
|
||||
|
||||
protected function processWorkload($job)
|
||||
{
|
||||
echo "Received job: " . $job->handle() . " for function " . $job->functionName() . "\n";
|
||||
|
||||
$workload = $job->workload();
|
||||
$workload = unserialize($workload);
|
||||
|
||||
if (!isset($workload['conn']) || !is_array($workload['conn'])) {
|
||||
throw new \InvalidArgumentException("Missing Database parameters");
|
||||
}
|
||||
|
||||
$this->em = $this->createEntityManager($workload['conn']);
|
||||
|
||||
if (!isset($workload['fixture'])) {
|
||||
throw new \InvalidArgumentException("Missing Fixture parameters");
|
||||
}
|
||||
return $workload['fixture'];
|
||||
}
|
||||
|
||||
protected function createEntityManager($conn)
|
||||
{
|
||||
$config = new \Doctrine\ORM\Configuration();
|
||||
$config->setProxyDir(__DIR__ . '/../../../Proxies');
|
||||
$config->setProxyNamespace('MyProject\Proxies');
|
||||
$config->setAutoGenerateProxyClasses(true);
|
||||
|
||||
$annotDriver = $config->newDefaultAnnotationDriver(array(__DIR__ . '/../../../Models/'));
|
||||
$config->setMetadataDriverImpl($annotDriver);
|
||||
|
||||
$cache = new \Doctrine\Common\Cache\ArrayCache();
|
||||
$config->setMetadataCacheImpl($cache);
|
||||
$config->setQueryCacheImpl($cache);
|
||||
$config->setSQLLogger(new \Doctrine\DBAL\Logging\EchoSQLLogger());
|
||||
|
||||
$em = \Doctrine\ORM\EntityManager::create($conn, $config);
|
||||
|
||||
return $em;
|
||||
}
|
||||
}
|
||||
|
||||
LockAgentWorker::run();
|
157
tests/Doctrine/Tests/ORM/Functional/Locking/LockTest.php
Normal file
157
tests/Doctrine/Tests/ORM/Functional/Locking/LockTest.php
Normal file
@ -0,0 +1,157 @@
|
||||
<?php
|
||||
|
||||
namespace Doctrine\Tests\ORM\Functional\Locking;
|
||||
|
||||
use Doctrine\Tests\Models\CMS\CmsArticle,
|
||||
Doctrine\Tests\Models\CMS\CmsUser,
|
||||
Doctrine\ORM\LockMode,
|
||||
Doctrine\ORM\EntityManager;
|
||||
|
||||
require_once __DIR__ . '/../../../TestInit.php';
|
||||
|
||||
class LockTest extends \Doctrine\Tests\OrmFunctionalTestCase {
|
||||
protected function setUp() {
|
||||
$this->useModelSet('cms');
|
||||
parent::setUp();
|
||||
$this->handles = array();
|
||||
}
|
||||
|
||||
/**
|
||||
* @group DDC-178
|
||||
* @group locking
|
||||
*/
|
||||
public function testLockVersionedEntity() {
|
||||
$article = new CmsArticle();
|
||||
$article->text = "my article";
|
||||
$article->topic = "Hello";
|
||||
|
||||
$this->_em->persist($article);
|
||||
$this->_em->flush();
|
||||
|
||||
$this->_em->lock($article, LockMode::OPTIMISTIC, $article->version);
|
||||
}
|
||||
|
||||
/**
|
||||
* @group DDC-178
|
||||
* @group locking
|
||||
*/
|
||||
public function testLockVersionedEntity_MissmatchThrowsException() {
|
||||
$article = new CmsArticle();
|
||||
$article->text = "my article";
|
||||
$article->topic = "Hello";
|
||||
|
||||
$this->_em->persist($article);
|
||||
$this->_em->flush();
|
||||
|
||||
$this->setExpectedException('Doctrine\ORM\OptimisticLockException');
|
||||
$this->_em->lock($article, LockMode::OPTIMISTIC, $article->version + 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* @group DDC-178
|
||||
* @group locking
|
||||
*/
|
||||
public function testLockUnversionedEntity_ThrowsException() {
|
||||
$user = new CmsUser();
|
||||
$user->name = "foo";
|
||||
$user->status = "active";
|
||||
$user->username = "foo";
|
||||
|
||||
$this->_em->persist($user);
|
||||
$this->_em->flush();
|
||||
|
||||
$this->setExpectedException('Doctrine\ORM\OptimisticLockException');
|
||||
$this->_em->lock($user, LockMode::OPTIMISTIC);
|
||||
}
|
||||
|
||||
/**
|
||||
* @group DDC-178
|
||||
* @group locking
|
||||
*/
|
||||
public function testLockUnmanagedEntity_ThrowsException() {
|
||||
$article = new CmsArticle();
|
||||
|
||||
$this->setExpectedException('InvalidArgumentException', 'Entity is not MANAGED.');
|
||||
$this->_em->lock($article, LockMode::OPTIMISTIC, $article->version + 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* @group DDC-178
|
||||
* @group locking
|
||||
*/
|
||||
public function testLockPessimisticRead_NoTransaction_ThrowsException() {
|
||||
$article = new CmsArticle();
|
||||
$article->text = "my article";
|
||||
$article->topic = "Hello";
|
||||
|
||||
$this->_em->persist($article);
|
||||
$this->_em->flush();
|
||||
|
||||
$this->setExpectedException('Doctrine\ORM\TransactionRequiredException');
|
||||
$this->_em->lock($article, LockMode::PESSIMISTIC_READ);
|
||||
}
|
||||
|
||||
/**
|
||||
* @group DDC-178
|
||||
* @group locking
|
||||
*/
|
||||
public function testLockPessimisticWrite_NoTransaction_ThrowsException() {
|
||||
$article = new CmsArticle();
|
||||
$article->text = "my article";
|
||||
$article->topic = "Hello";
|
||||
|
||||
$this->_em->persist($article);
|
||||
$this->_em->flush();
|
||||
|
||||
$this->setExpectedException('Doctrine\ORM\TransactionRequiredException');
|
||||
$this->_em->lock($article, LockMode::PESSIMISTIC_WRITE);
|
||||
}
|
||||
|
||||
/**
|
||||
* @group DDC-178
|
||||
* @group locking
|
||||
*/
|
||||
public function testLockPessimisticWrite() {
|
||||
$writeLockSql = $this->_em->getConnection()->getDatabasePlatform()->getWriteLockSql();
|
||||
if (strlen($writeLockSql) == 0) {
|
||||
$this->markTestSkipped('Database Driver has no Write Lock support.');
|
||||
}
|
||||
|
||||
$article = new CmsArticle();
|
||||
$article->text = "my article";
|
||||
$article->topic = "Hello";
|
||||
|
||||
$this->_em->persist($article);
|
||||
$this->_em->flush();
|
||||
|
||||
$this->_em->beginTransaction();
|
||||
$this->_em->lock($article, LockMode::PESSIMISTIC_WRITE);
|
||||
|
||||
$query = array_pop( $this->_sqlLoggerStack->queries );
|
||||
$this->assertContains($writeLockSql, $query['sql']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @group DDC-178
|
||||
* @group locking
|
||||
*/
|
||||
public function testLockPessimisticRead() {
|
||||
$readLockSql = $this->_em->getConnection()->getDatabasePlatform()->getReadLockSql();
|
||||
if (strlen($readLockSql) == 0) {
|
||||
$this->markTestSkipped('Database Driver has no Write Lock support.');
|
||||
}
|
||||
|
||||
$article = new CmsArticle();
|
||||
$article->text = "my article";
|
||||
$article->topic = "Hello";
|
||||
|
||||
$this->_em->persist($article);
|
||||
$this->_em->flush();
|
||||
|
||||
$this->_em->beginTransaction();
|
||||
$this->_em->lock($article, LockMode::PESSIMISTIC_READ);
|
||||
|
||||
$query = array_pop( $this->_sqlLoggerStack->queries );
|
||||
$this->assertContains($readLockSql, $query['sql']);
|
||||
}
|
||||
}
|
@ -15,13 +15,16 @@ 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) {
|
||||
@ -57,7 +60,7 @@ class SelectSqlGenerationTest extends \Doctrine\Tests\OrmTestCase
|
||||
{
|
||||
$this->assertSqlGeneration(
|
||||
'SELECT a FROM Doctrine\Tests\Models\CMS\CmsArticle a ORDER BY a.user.name ASC',
|
||||
'SELECT c0_.id AS id0, c0_.topic AS topic1, c0_.text AS text2 FROM cms_articles c0_ INNER JOIN cms_users c1_ ON c0_.user_id = c1_.id ORDER BY c1_.name ASC'
|
||||
'SELECT c0_.id AS id0, c0_.topic AS topic1, c0_.text AS text2, c0_.version AS version3 FROM cms_articles c0_ INNER JOIN cms_users c1_ ON c0_.user_id = c1_.id ORDER BY c1_.name ASC'
|
||||
);
|
||||
}
|
||||
|
||||
@ -181,11 +184,11 @@ class SelectSqlGenerationTest extends \Doctrine\Tests\OrmTestCase
|
||||
);
|
||||
}
|
||||
|
||||
public function testSupportsMultipleEntitesInFromClause()
|
||||
public function testSupportsMultipleEntitiesInFromClause()
|
||||
{
|
||||
$this->assertSqlGeneration(
|
||||
'SELECT u, a FROM Doctrine\Tests\Models\CMS\CmsUser u, Doctrine\Tests\Models\CMS\CmsArticle a WHERE u.id = a.user.id',
|
||||
'SELECT c0_.id AS id0, c0_.status AS status1, c0_.username AS username2, c0_.name AS name3, c1_.id AS id4, c1_.topic AS topic5, c1_.text AS text6 FROM cms_users c0_ INNER JOIN cms_users c2_ ON c1_.user_id = c2_.id WHERE c0_.id = c2_.id'
|
||||
'SELECT c0_.id AS id0, c0_.status AS status1, c0_.username AS username2, c0_.name AS name3, c1_.id AS id4, c1_.topic AS topic5, c1_.text AS text6, c1_.version AS version7 FROM cms_users c0_ INNER JOIN cms_users c2_ ON c1_.user_id = c2_.id WHERE c0_.id = c2_.id'
|
||||
);
|
||||
}
|
||||
|
||||
@ -587,7 +590,41 @@ class SelectSqlGenerationTest extends \Doctrine\Tests\OrmTestCase
|
||||
}
|
||||
|
||||
/**
|
||||
* DDC-430
|
||||
* @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 DDC-430
|
||||
*/
|
||||
public function testSupportSelectWithMoreThan10InputParameters()
|
||||
{
|
||||
@ -598,7 +635,39 @@ class SelectSqlGenerationTest extends \Doctrine\Tests\OrmTestCase
|
||||
}
|
||||
|
||||
/**
|
||||
* DDC-431
|
||||
* @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)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @group DDC-431
|
||||
*/
|
||||
public function testSupportToCustomDQLFunctions()
|
||||
{
|
||||
|
25
tests/README.markdown
Normal file
25
tests/README.markdown
Normal file
@ -0,0 +1,25 @@
|
||||
# Running the Doctrine 2 Testsuite
|
||||
|
||||
## Setting up a PHPUnit Configuration XML
|
||||
|
||||
..
|
||||
|
||||
## Testing Lock-Support
|
||||
|
||||
The Lock support in Doctrine 2 is tested using Gearman, which allows to run concurrent tasks in parallel.
|
||||
Install Gearman with PHP as follows:
|
||||
|
||||
1. Go to http://www.gearman.org and download the latest Gearman Server
|
||||
2. Compile it and then call ldconfig
|
||||
3. Start it up "gearmand -vvvv"
|
||||
4. Install pecl/gearman by calling "gearman-beta"
|
||||
|
||||
You can then go into tests/ and start up two workers:
|
||||
|
||||
php Doctrine/Tests/ORM/Functional/Locking/LockAgentWorker.php
|
||||
|
||||
Then run the locking test-suite:
|
||||
|
||||
phpunit --configuration <myconfig.xml> Doctrine/Tests/ORM/Functional/Locking/GearmanLockTest.php
|
||||
|
||||
This can run considerable time, because it is using sleep() to test for the timing ranges of locks.
|
Loading…
Reference in New Issue
Block a user