Commit e9583964 authored by Benjamin Eberlei's avatar Benjamin Eberlei

Merge branch 'master' into develop

parents 3b01277f 28b03077
<?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
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';
}
}
\ No newline at end of file
......@@ -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);
}
}
/**
......
<?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
......@@ -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);
}
}
\ No newline at end of file
......@@ -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()
......
<?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
......@@ -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));
}
/**
......
<?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
......@@ -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;
}
......
<?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());
}
}
\ No newline at end of file
<?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();
\ No newline at end of file
<?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']);
}
}
\ No newline at end of file
......@@ -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()
{
......
# 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.
\ No newline at end of file
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