Commit 3cbee1fa authored by Roman S. Borschel's avatar Roman S. Borschel

Merge commit 'upstream/master'

parents 3045507a d098d62e
......@@ -753,7 +753,7 @@ class Connection implements DriverConnection
public function commit()
{
if ($this->_transactionNestingLevel == 0) {
throw ConnectionException::commitFailedNoActiveTransaction();
throw ConnectionException::noActiveTransaction();
}
if ($this->_isRollbackOnly) {
throw ConnectionException::commitFailedRollbackOnly();
......@@ -779,7 +779,7 @@ class Connection implements DriverConnection
public function rollback()
{
if ($this->_transactionNestingLevel == 0) {
throw ConnectionException::rollbackFailedNoActiveTransaction();
throw ConnectionException::noActiveTransaction();
}
$this->connect();
......
......@@ -175,7 +175,8 @@ class OCI8Statement implements \Doctrine\DBAL\Driver\Statement
}
$result = array();
oci_fetch_all($this->_sth, $result, 0, -1, self::$fetchStyleMap[$fetchStyle] | OCI_RETURN_NULLS | OCI_FETCHSTATEMENT_BY_ROW);
oci_fetch_all($this->_sth, $result, 0, -1,
self::$fetchStyleMap[$fetchStyle] | OCI_RETURN_NULLS | OCI_FETCHSTATEMENT_BY_ROW | OCI_RETURN_LOBS);
return $result;
}
......
<?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 '';
}
}
......@@ -463,7 +463,7 @@ abstract class AbstractQuery
public function iterate(array $params = array(), $hydrationMode = self::HYDRATE_OBJECT)
{
return $this->_em->newHydrator($this->_hydrationMode)->iterate(
$this->_doExecute($params, $hydrationMode), $this->_resultSetMapping
$this->_doExecute($params, $hydrationMode), $this->_resultSetMapping, $this->_hints
);
}
......
......@@ -22,6 +22,7 @@ namespace Doctrine\ORM;
use Closure, Exception,
Doctrine\Common\EventManager,
Doctrine\DBAL\Connection,
Doctrine\DBAL\LockMode,
Doctrine\ORM\Mapping\ClassMetadata,
Doctrine\ORM\Mapping\ClassMetadataFactory,
Doctrine\ORM\Proxy\ProxyFactory;
......@@ -318,11 +319,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 +481,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.
*
......
......@@ -19,6 +19,8 @@
namespace Doctrine\ORM;
use Doctrine\DBAL\LockMode;
/**
* An EntityRepository serves as a repository for entities with generic as well as
* business specific methods for retrieving entities.
......@@ -87,23 +89,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);
}
}
/**
......
......@@ -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\DBAL\LockMode::PESSIMISTIC_READ) {
$lockSql = ' ' . $this->_platform->getReadLockSql();
} else if ($lockMode == \Doctrine\DBAL\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\DBAL\LockMode::PESSIMISTIC_READ) {
$lockSql = $this->_platform->getReadLockSql();
} else if ($lockMode == \Doctrine\DBAL\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
......@@ -19,7 +19,8 @@
namespace Doctrine\ORM;
use Doctrine\ORM\Query\Parser,
use Doctrine\DBAL\LockMode,
Doctrine\ORM\Query\Parser,
Doctrine\ORM\Query\QueryException;
/**
......@@ -93,6 +94,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 +493,39 @@ final class Query extends AbstractQuery
return parent::setHydrationMode($hydrationMode);
}
/**
* Set the lock mode for this Query.
*
* @see Doctrine\DBAL\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,20 @@ class SqlWalker implements TreeWalker
$sql, $this->_query->getMaxResults(), $this->_query->getFirstResult()
);
if (($lockMode = $this->_query->getHint(Query::HINT_LOCK_MODE)) !== false) {
if ($lockMode == \Doctrine\DBAL\LockMode::PESSIMISTIC_READ) {
$sql .= " " . $this->_platform->getReadLockSQL();
} else if ($lockMode == \Doctrine\DBAL\LockMode::PESSIMISTIC_WRITE) {
$sql .= " " . $this->_platform->getWriteLockSQL();
} else if ($lockMode == \Doctrine\DBAL\LockMode::OPTIMISTIC) {
foreach ($this->_selectedClasses AS $class) {
if (!$class->isVersioned) {
throw \Doctrine\ORM\OptimisticLockException::lockFailed();
}
}
}
}
return $sql;
}
......@@ -603,7 +617,7 @@ class SqlWalker implements TreeWalker
$sql .= $this->walkJoinVariableDeclaration($joinVarDecl);
}
return $sql;
return $this->_platform->appendLockHint($sql, $this->_query->getHint(Query::HINT_LOCK_MODE));
}
/**
......
......@@ -93,9 +93,7 @@ class SchemaValidator
if (!$targetMetadata->hasAssociation($assoc->mappedBy)) {
$ce[] = "The association " . $class->name . "#" . $fieldName . " refers to the owning side ".
"field " . $assoc->targetEntityName . "#" . $assoc->mappedBy . " which does not exist.";
}
if ($targetMetadata->associationMappings[$assoc->mappedBy]->inversedBy == null) {
} else if ($targetMetadata->associationMappings[$assoc->mappedBy]->inversedBy == null) {
$ce[] = "The field " . $class->name . "#" . $fieldName . " is on the inverse side of a ".
"bi-directional relationship, but the specified mappedBy association on the target-entity ".
$assoc->targetEntityName . "#" . $assoc->mappedBy . " does not contain the required ".
......@@ -115,11 +113,8 @@ class SchemaValidator
if (!$targetMetadata->hasAssociation($assoc->inversedBy)) {
$ce[] = "The association " . $class->name . "#" . $fieldName . " refers to the inverse side ".
"field " . $assoc->targetEntityName . "#" . $assoc->inversedBy . " which does not exist.";
}
if (isset($targetMetadata->associationMappings[$assoc->mappedBy]) &&
$targetMetadata->associationMappings[$assoc->mappedBy]->mappedBy == null) {
$ce[] = "The field " . $class->name . "#" . $fieldName . " is on the inverse side of a ".
} else if ($targetMetadata->associationMappings[$assoc->inversedBy]->mappedBy == null) {
$ce[] = "The field " . $class->name . "#" . $fieldName . " is on the owning side of a ".
"bi-directional relationship, but the specified mappedBy association on the target-entity ".
$assoc->targetEntityName . "#" . $assoc->mappedBy . " does not contain the required ".
"'inversedBy' attribute.";
......@@ -175,8 +170,6 @@ class SchemaValidator
}
}
}
} else {
}
if ($ce) {
......
<?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 == \Doctrine\DBAL\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 (in_array($lockMode, array(\Doctrine\DBAL\LockMode::PESSIMISTIC_READ, \Doctrine\DBAL\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.
*
......@@ -1733,6 +1775,10 @@ class UnitOfWork implements PropertyChangedListener
if ($entity instanceof Proxy && ! $entity->__isInitialized__) {
$entity->__isInitialized__ = true;
$overrideLocalValues = true;
$this->_originalEntityData[$oid] = $data;
if ($entity instanceof NotifyPropertyChanged) {
$entity->addPropertyChangedListener($this);
}
} else {
$overrideLocalValues = isset($hints[Query::HINT_REFRESH]);
}
......@@ -1802,6 +1848,7 @@ class UnitOfWork implements PropertyChangedListener
$this->_entityIdentifiers[$newValueOid] = $associatedId;
$this->_identityMap[$targetClass->rootEntityName][$relatedIdHash] = $newValue;
$this->_entityStates[$newValueOid] = self::STATE_MANAGED;
// make sure that when an proxy is then finally loaded, $this->_originalEntityData is set also!
}
}
$this->_originalEntityData[$oid][$field] = $newValue;
......
......@@ -11,6 +11,11 @@ use Doctrine\DBAL\Events;
class ConnectionTest extends \Doctrine\Tests\DbalTestCase
{
/**
* @var Doctrine\DBAL\Connection
*/
protected $_conn = null;
public function setUp()
{
$params = array(
......@@ -23,6 +28,47 @@ class ConnectionTest extends \Doctrine\Tests\DbalTestCase
$this->_conn = \Doctrine\DBAL\DriverManager::getConnection($params);
}
public function testIsConnected()
{
$this->assertFalse($this->_conn->isConnected());
}
public function testNoTransactionActiveByDefault()
{
$this->assertFalse($this->_conn->isTransactionActive());
}
public function testCommitWithNoActiveTransaction_ThrowsException()
{
$this->setExpectedException('Doctrine\DBAL\ConnectionException');
$this->_conn->commit();
}
public function testRollbackWithNoActiveTransaction_ThrowsException()
{
$this->setExpectedException('Doctrine\DBAL\ConnectionException');
$this->_conn->rollback();
}
public function testSetRollbackOnlyNoActiveTransaction_ThrowsException()
{
$this->setExpectedException('Doctrine\DBAL\ConnectionException');
$this->_conn->setRollbackOnly();
}
public function testIsRollbackOnlyNoActiveTransaction_ThrowsException()
{
$this->setExpectedException('Doctrine\DBAL\ConnectionException');
$this->_conn->isRollbackOnly();
}
public function testGetConfiguration()
{
$config = $this->_conn->getConfiguration();
$this->assertType('Doctrine\DBAL\Configuration', $config);
}
public function testGetHost()
{
$this->assertEquals('localhost', $this->_conn->getHost());
......
......@@ -28,6 +28,7 @@ class AllTests
$suite->addTestSuite('Doctrine\Tests\DBAL\Functional\Schema\Db2SchemaManagerTest');
$suite->addTestSuite('Doctrine\Tests\DBAL\Functional\ConnectionTest');
$suite->addTestSuite('Doctrine\Tests\DBAL\Functional\DataAccessTest');
$suite->addTestSuite('Doctrine\Tests\DBAL\Functional\WriteTest');
return $suite;
}
......
......@@ -8,7 +8,25 @@ require_once __DIR__ . '/../../TestInit.php';
class ConnectionTest extends \Doctrine\Tests\DbalFunctionalTestCase
{
public function setUp()
{
$this->resetSharedConn();
parent::setUp();
}
public function testGetWrappedConnection()
{
$this->assertType('Doctrine\DBAL\Driver\Connection', $this->_conn->getWrappedConnection());
}
public function testCommitWithRollbackOnlyThrowsException()
{
$this->_conn->beginTransaction();
$this->_conn->setRollbackOnly();
$this->setExpectedException('Doctrine\DBAL\ConnectionException');
$this->_conn->commit();
}
public function testTransactionNestingBehavior()
{
try {
......@@ -36,7 +54,7 @@ class ConnectionTest extends \Doctrine\Tests\DbalFunctionalTestCase
}
}
public function testTransactionBehavior()
public function testTransactionBehaviorWithRollback()
{
try {
$this->_conn->beginTransaction();
......@@ -50,7 +68,10 @@ class ConnectionTest extends \Doctrine\Tests\DbalFunctionalTestCase
$this->_conn->rollback();
$this->assertEquals(0, $this->_conn->getTransactionNestingLevel());
}
}
public function testTransactionBehaviour()
{
try {
$this->_conn->beginTransaction();
$this->assertEquals(1, $this->_conn->getTransactionNestingLevel());
......@@ -61,6 +82,10 @@ class ConnectionTest extends \Doctrine\Tests\DbalFunctionalTestCase
}
$this->assertEquals(0, $this->_conn->getTransactionNestingLevel());
}
public function testTransactionalWithException()
{
try {
$this->_conn->transactional(function($conn) {
$conn->executeQuery("select 1");
......@@ -70,5 +95,11 @@ class ConnectionTest extends \Doctrine\Tests\DbalFunctionalTestCase
$this->assertEquals(0, $this->_conn->getTransactionNestingLevel());
}
}
public function testTransactional()
{
$this->_conn->transactional(function($conn) {
$conn->executeQuery("select 1");
});
}
}
\ No newline at end of file
......@@ -25,6 +25,101 @@ class DataAccessTest extends \Doctrine\Tests\DbalFunctionalTestCase
}
}
public function testPrepareWithBindValue()
{
$sql = "SELECT test_int, test_string FROM fetch_table WHERE test_int = ? AND test_string = ?";
$stmt = $this->_conn->prepare($sql);
$this->assertType('Doctrine\DBAL\Statement', $stmt);
$stmt->bindValue(1, 1);
$stmt->bindValue(2, 'foo');
$stmt->execute();
$row = $stmt->fetch(\PDO::FETCH_ASSOC);
$row = array_change_key_case($row, \CASE_LOWER);
$this->assertEquals(array('test_int' => 1, 'test_string' => 'foo'), $row);
}
public function testPrepareWithBindParam()
{
$paramInt = 1;
$paramStr = 'foo';
$sql = "SELECT test_int, test_string FROM fetch_table WHERE test_int = ? AND test_string = ?";
$stmt = $this->_conn->prepare($sql);
$this->assertType('Doctrine\DBAL\Statement', $stmt);
$stmt->bindParam(1, $paramInt);
$stmt->bindParam(2, $paramStr);
$stmt->execute();
$row = $stmt->fetch(\PDO::FETCH_ASSOC);
$row = array_change_key_case($row, \CASE_LOWER);
$this->assertEquals(array('test_int' => 1, 'test_string' => 'foo'), $row);
}
public function testPrepareWithFetchAll()
{
$paramInt = 1;
$paramStr = 'foo';
$sql = "SELECT test_int, test_string FROM fetch_table WHERE test_int = ? AND test_string = ?";
$stmt = $this->_conn->prepare($sql);
$this->assertType('Doctrine\DBAL\Statement', $stmt);
$stmt->bindParam(1, $paramInt);
$stmt->bindParam(2, $paramStr);
$stmt->execute();
$rows = $stmt->fetchAll(\PDO::FETCH_ASSOC);
$rows[0] = array_change_key_case($rows[0], \CASE_LOWER);
$this->assertEquals(array('test_int' => 1, 'test_string' => 'foo'), $rows[0]);
}
public function testPrepareWithFetchColumn()
{
$paramInt = 1;
$paramStr = 'foo';
$sql = "SELECT test_int FROM fetch_table WHERE test_int = ? AND test_string = ?";
$stmt = $this->_conn->prepare($sql);
$this->assertType('Doctrine\DBAL\Statement', $stmt);
$stmt->bindParam(1, $paramInt);
$stmt->bindParam(2, $paramStr);
$stmt->execute();
$column = $stmt->fetchColumn();
$this->assertEquals(1, $column);
}
public function testPrepareWithQuoted()
{
$table = 'fetch_table';
$paramInt = 1;
$paramStr = 'foo';
$sql = "SELECT test_int, test_string FROM " . $this->_conn->quoteIdentifier($table) . " ".
"WHERE test_int = " . $this->_conn->quote($paramInt) . " AND test_string = " . $this->_conn->quote($paramStr);
$stmt = $this->_conn->prepare($sql);
$this->assertType('Doctrine\DBAL\Statement', $stmt);
}
public function testPrepareWithExecuteParams()
{
$paramInt = 1;
$paramStr = 'foo';
$sql = "SELECT test_int, test_string FROM fetch_table WHERE test_int = ? AND test_string = ?";
$stmt = $this->_conn->prepare($sql);
$this->assertType('Doctrine\DBAL\Statement', $stmt);
$stmt->execute(array($paramInt, $paramStr));
$row = $stmt->fetch(\PDO::FETCH_ASSOC);
$row = array_change_key_case($row, \CASE_LOWER);
$this->assertEquals(array('test_int' => 1, 'test_string' => 'foo'), $row);
}
public function testFetchAll()
{
$sql = "SELECT test_int, test_string FROM fetch_table WHERE test_int = ? AND test_string = ?";
......@@ -60,4 +155,16 @@ class DataAccessTest extends \Doctrine\Tests\DbalFunctionalTestCase
$this->assertEquals('foo', $row[1]);
}
public function testFetchColumn()
{
$sql = "SELECT test_int, test_string FROM fetch_table WHERE test_int = ? AND test_string = ?";
$testInt = $this->_conn->fetchColumn($sql, array(1, 'foo'), 0);
$this->assertEquals(1, $testInt);
$sql = "SELECT test_int, test_string FROM fetch_table WHERE test_int = ? AND test_string = ?";
$testString = $this->_conn->fetchColumn($sql, array(1, 'foo'), 1);
$this->assertEquals('foo', $testString);
}
}
\ No newline at end of file
<?php
namespace Doctrine\Tests\DBAL\Functional;
use Doctrine\DBAL\Types\Type;
require_once __DIR__ . '/../../TestInit.php';
class WriteTest extends \Doctrine\Tests\DbalFunctionalTestCase
{
public function setUp()
{
parent::setUp();
try {
/* @var $sm \Doctrine\DBAL\Schema\AbstractSchemaManager */
$table = new \Doctrine\DBAL\Schema\Table("write_table");
$table->addColumn('test_int', 'integer');
$table->addColumn('test_string', 'string', array('notnull' => false));
$sm = $this->_conn->getSchemaManager();
$sm->createTable($table);
} catch(\Exception $e) {
}
$this->_conn->executeUpdate('DELETE FROM write_table');
}
public function testExecuteUpdate()
{
$sql = "INSERT INTO " . $this->_conn->quoteIdentifier('write_table') . " ( " .
$this->_conn->quoteIdentifier('test_int') . " ) VALUES ( " . $this->_conn->quote(1) . ")";
$affected = $this->_conn->executeUpdate($sql);
$this->assertEquals(1, $affected, "executeUpdate() should return the number of affected rows!");
}
public function testExecuteUpdateWithTypes()
{
$sql = "INSERT INTO write_table (test_int, test_string) VALUES (?, ?)";
$affected = $this->_conn->executeUpdate($sql, array(1, 'foo'), array(\PDO::PARAM_INT, \PDO::PARAM_STR));
$this->assertEquals(1, $affected, "executeUpdate() should return the number of affected rows!");
}
public function testPrepareRowCountReturnsAffectedRows()
{
$sql = "INSERT INTO write_table (test_int, test_string) VALUES (?, ?)";
$stmt = $this->_conn->prepare($sql);
$stmt->bindValue(1, 1);
$stmt->bindValue(2, "foo");
$stmt->execute();
$this->assertEquals(1, $stmt->rowCount());
}
public function testPrepareWithPdoTypes()
{
$sql = "INSERT INTO write_table (test_int, test_string) VALUES (?, ?)";
$stmt = $this->_conn->prepare($sql);
$stmt->bindValue(1, 1, \PDO::PARAM_INT);
$stmt->bindValue(2, "foo", \PDO::PARAM_STR);
$stmt->execute();
$this->assertEquals(1, $stmt->rowCount());
}
public function testPrepareWithDbalTypes()
{
$sql = "INSERT INTO write_table (test_int, test_string) VALUES (?, ?)";
$stmt = $this->_conn->prepare($sql);
$stmt->bindValue(1, 1, Type::getType('integer'));
$stmt->bindValue(2, "foo", Type::getType('string'));
$stmt->execute();
$this->assertEquals(1, $stmt->rowCount());
}
public function testPrepareWithDbalTypeNames()
{
$sql = "INSERT INTO write_table (test_int, test_string) VALUES (?, ?)";
$stmt = $this->_conn->prepare($sql);
$stmt->bindValue(1, 1, 'integer');
$stmt->bindValue(2, "foo", 'string');
$stmt->execute();
$this->assertEquals(1, $stmt->rowCount());
}
public function insertRows()
{
$this->assertEquals(1, $this->_conn->insert('write_table', array('test_int' => 1)));
$this->assertEquals(1, $this->_conn->insert('write_table', array('test_int' => 2)));
}
public function testInsert()
{
$this->insertRows();
}
public function testDelete()
{
$this->insertRows();
$this->assertEquals(1, $this->_conn->delete('write_table', array('test_int' => 2)));
$this->assertEquals(1, count($this->_conn->fetchAll('SELECT * FROM write_table')));
$this->assertEquals(1, $this->_conn->delete('write_table', array('test_int' => 1)));
$this->assertEquals(0, count($this->_conn->fetchAll('SELECT * FROM write_table')));
}
public function testUpdate()
{
$this->insertRows();
$this->assertEquals(1, $this->_conn->update('write_table', array('test_int' => 2), array('test_int' => 1)));
$this->assertEquals(2, $this->_conn->update('write_table', array('test_int' => 3), array('test_int' => 2)));
}
}
\ No newline at end of file
......@@ -12,6 +12,12 @@ class DbalFunctionalTestCase extends DbalTestCase
*/
protected $_conn;
protected function resetSharedConn()
{
$this->sharedFixture['conn'] = null;
self::$_sharedConn = null;
}
protected function setUp()
{
if (isset($this->sharedFixture['conn'])) {
......
......@@ -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\DBAL\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\DBAL\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\DBAL\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\DBAL\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\DBAL\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\DBAL\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
<?php
namespace Doctrine\Tests\ORM\Functional\Ticket;
require_once __DIR__ . '/../../../TestInit.php';
class DDC440Test extends \Doctrine\Tests\OrmFunctionalTestCase
{
protected function setUp()
{
parent::setUp();
try {
$this->_schemaTool->createSchema(array(
$this->_em->getClassMetadata('Doctrine\Tests\ORM\Functional\Ticket\DDC440Phone'),
$this->_em->getClassMetadata('Doctrine\Tests\ORM\Functional\Ticket\DDC440Client')
));
} catch (\Exception $e) {
// Swallow all exceptions. We do not test the schema tool here.
}
}
/**
* @group DDC-440
*/
public function testOriginalEntityDataEmptyWhenProxyLoadedFromTwoAssociations()
{
/* The key of the problem is that the first phone is fetched via two association, main_phone and phones.
*
* You will notice that the original_entity_datas are not loaded for the first phone. (They are for the second)
*
* In the Client entity definition, if you define the main_phone relation after the phones relation, both assertions pass.
* (for the sake or this test, I defined the main_phone relation before the phones relation)
*
*/
//Initialize some data
$client = new DDC440Client;
$client->setName('Client1');
$phone = new DDC440Phone;
$phone->setNumber('418 111-1111');
$phone->setClient($client);
$phone2 = new DDC440Phone;
$phone2->setNumber('418 222-2222');
$phone2->setClient($client);
$client->setMainPhone($phone);
$this->_em->persist($client);
$this->_em->flush();
$id = $client->getId();
$this->_em->clear();
$uw = $this->_em->getUnitOfWork();
$client = $this->_em->find('Doctrine\Tests\ORM\Functional\Ticket\DDC440Client', $id);
$clientPhones = $client->getPhones();
$p1 = $clientPhones[0];
$p2 = $clientPhones[1];
// Test the first phone. The assertion actually failed because original entity data is not set properly.
// This was because it is also set as MainPhone and that one is created as a proxy, not the
// original object when the find on Client is called. However loading proxies did not work correctly.
$this->assertType('Doctrine\Tests\ORM\Functional\Ticket\DDC440Phone', $p1);
$originalData = $uw->getOriginalEntityData($p1);
$this->assertEquals($phone->getNumber(), $originalData['number']);
//If you comment out previous test, this one should pass
$this->assertType('Doctrine\Tests\ORM\Functional\Ticket\DDC440Phone', $p2);
$originalData = $uw->getOriginalEntityData($p2);
$this->assertEquals($phone2->getNumber(), $originalData['number']);
}
}
/**
* @Entity
* @Table(name="phone")
*/
class DDC440Phone {
/**
* @Column(name="id", type="integer")
* @Id
* @GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* @ManyToOne(targetEntity="DDC440Client",inversedBy="phones")
* @JoinColumns({
* @JoinColumn(name="client_id", referencedColumnName="id")
* })
*/
protected $client;
/**
* @Column(name="number", type="string")
*/
protected $number;
public function setNumber($value){
$this->number = $value;
}
public function getNumber(){
return $this->number;
}
public function setClient(DDC440Client $value, $update_inverse=true)
{
$this->client = $value;
if($update_inverse){
$value->addPhone($this);
}
}
public function getClient()
{
return $this->client;
}
public function getId()
{
return $this->id;
}
public function setId($value){
$this->id = $value;
}
}
/**
* @Entity
* @Table(name="client")
*/
class DDC440Client {
/**
* @Column(name="id", type="integer")
* @Id
* @GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* @OneToOne(targetEntity="DDC440Phone", fetch="EAGER")
* @JoinColumns({
* @JoinColumn(name="main_phone_id", referencedColumnName="id",onDelete="SET NULL")
* })
*/
protected $main_phone;
/**
* @OneToMany(targetEntity="DDC440Phone", mappedBy="client", cascade={"persist", "remove"}, fetch="EAGER")
*/
protected $phones;
/**
* @Column(name="name", type="string")
*/
protected $name;
public function __construct(){
}
public function setName($value){
$this->name = $value;
}
public function getName(){
return $this->name;
}
public function addPhone(DDC440Phone $value)
{
$this->phones[] = $value;
$value->setClient($this, false);
}
public function getPhones()
{
return $this->phones;
}
public function setMainPhone(DDC440Phone $value){
$this->main_phone = $value;
}
public function getMainPhone(){
return $this->main_phone;
}
public function getId()
{
return $this->id;
}
public function setId($value){
$this->id = $value;
}
}
......@@ -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'
);
}
......@@ -598,7 +601,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\DBAL\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\DBAL\LockMode::PESSIMISTIC_READ)
);
}
/**
* @group DDC-430
*/
public function testSupportSelectWithMoreThan10InputParameters()
{
......@@ -609,7 +646,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\DBAL\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\DBAL\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