Merge commit 'upstream/master'
This commit is contained in:
commit
3cbee1fa09
@ -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;
|
||||
}
|
||||
|
42
lib/Doctrine/DBAL/LockMode.php
Normal file
42
lib/Doctrine/DBAL/LockMode.php
Normal file
@ -0,0 +1,42 @@
|
||||
<?php
|
||||
/*
|
||||
* $Id$
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*
|
||||
* This software consists of voluntary contributions made by many individuals
|
||||
* and is licensed under the LGPL. For more information, see
|
||||
* <http://www.doctrine-project.org>.
|
||||
*/
|
||||
|
||||
namespace Doctrine\DBAL;
|
||||
|
||||
/**
|
||||
* Contains all ORM LockModes
|
||||
*
|
||||
* @license http://www.opensource.org/licenses/lgpl-license.php LGPL
|
||||
* @link www.doctrine-project.com
|
||||
* @since 1.0
|
||||
* @version $Revision$
|
||||
* @author Benjamin Eberlei <kontakt@beberlei.de>
|
||||
* @author Roman Borschel <roman@code-factory.org>
|
||||
*/
|
||||
class LockMode
|
||||
{
|
||||
const NONE = 0;
|
||||
const OPTIMISTIC = 1;
|
||||
const PESSIMISTIC_READ = 2;
|
||||
const PESSIMISTIC_WRITE = 4;
|
||||
|
||||
final private function __construct() { }
|
||||
}
|
@ -488,11 +488,48 @@ abstract class AbstractPlatform
|
||||
return 'COS(' . $value . ')';
|
||||
}
|
||||
|
||||
public function getForUpdateSql()
|
||||
public function getForUpdateSQL()
|
||||
{
|
||||
return 'FOR UPDATE';
|
||||
}
|
||||
|
||||
/**
|
||||
* Honors that some SQL vendors such as MsSql use table hints for locking instead of the ANSI SQL FOR UPDATE specification.
|
||||
*
|
||||
* @param string $fromClause
|
||||
* @param int $lockMode
|
||||
* @return string
|
||||
*/
|
||||
public function appendLockHint($fromClause, $lockMode)
|
||||
{
|
||||
return $fromClause;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the sql snippet to append to any SELECT statement which locks rows in shared read lock.
|
||||
*
|
||||
* This defaults to the ASNI SQL "FOR UPDATE", which is an exclusive lock (Write). Some database
|
||||
* vendors allow to lighten this constraint up to be a real read lock.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getReadLockSQL()
|
||||
{
|
||||
return $this->getForUpdateSQL();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the SQL snippet to append to any SELECT statement which obtains an exclusive lock on the rows.
|
||||
*
|
||||
* The semantics of this lock mode should equal the SELECT .. FOR UPDATE of the ASNI SQL standard.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getWriteLockSQL()
|
||||
{
|
||||
return $this->getForUpdateSQL();
|
||||
}
|
||||
|
||||
public function getDropDatabaseSQL($database)
|
||||
{
|
||||
return 'DROP DATABASE ' . $database;
|
||||
|
@ -513,4 +513,9 @@ class DB2Platform extends AbstractPlatform
|
||||
{
|
||||
return strtoupper($column);
|
||||
}
|
||||
|
||||
public function getForUpdateSQL()
|
||||
{
|
||||
return ' WITH RR USE AND KEEP UPDATE LOCKS';
|
||||
}
|
||||
}
|
@ -483,4 +483,32 @@ class MsSqlPlatform extends AbstractPlatform
|
||||
{
|
||||
return 'TRUNCATE TABLE '.$tableName;
|
||||
}
|
||||
|
||||
/**
|
||||
* MsSql uses Table Hints for locking strategies instead of the ANSI SQL FOR UPDATE like hints.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getForUpdateSQL()
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* @license LGPL
|
||||
* @author Hibernate
|
||||
* @param string $fromClause
|
||||
* @param int $lockMode
|
||||
* @return string
|
||||
*/
|
||||
public function appendLockHint($fromClause, $lockMode)
|
||||
{
|
||||
if ($lockMode == \Doctrine\DBAL\LockMode::PESSIMISTIC_WRITE) {
|
||||
return $fromClause . " WITH (UPDLOCK, ROWLOCK)";
|
||||
} else if ( $lockMode == \Doctrine\DBAL\LockMode::PESSIMISTIC_READ ) {
|
||||
return $fromClause . " WITH (HOLDLOCK, ROWLOCK)";
|
||||
} else {
|
||||
return $fromClause;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -583,4 +583,9 @@ class MySqlPlatform extends AbstractPlatform
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function getReadLockSQL()
|
||||
{
|
||||
return 'LOCK IN SHARE MODE';
|
||||
}
|
||||
}
|
||||
|
@ -637,4 +637,9 @@ class PostgreSqlPlatform extends AbstractPlatform
|
||||
{
|
||||
return 'TRUNCATE '.$tableName.' '.($cascade)?'CASCADE':'';
|
||||
}
|
||||
|
||||
public function getReadLockSQL()
|
||||
{
|
||||
return 'FOR SHARE';
|
||||
}
|
||||
}
|
||||
|
@ -428,4 +428,9 @@ class SqlitePlatform extends AbstractPlatform
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
public function getForUpdateSql()
|
||||
{
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
@ -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()
|
||||
|
40
lib/Doctrine/ORM/PessimisticLockException.php
Normal file
40
lib/Doctrine/ORM/PessimisticLockException.php
Normal file
@ -0,0 +1,40 @@
|
||||
<?php
|
||||
/*
|
||||
* $Id$
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*
|
||||
* This software consists of voluntary contributions made by many individuals
|
||||
* and is licensed under the LGPL. For more information, see
|
||||
* <http://www.doctrine-project.org>.
|
||||
*/
|
||||
|
||||
namespace Doctrine\ORM;
|
||||
|
||||
/**
|
||||
* Pessimistic Lock Exception
|
||||
*
|
||||
* @license http://www.opensource.org/licenses/lgpl-license.php LGPL
|
||||
* @link www.doctrine-project.com
|
||||
* @since 1.0
|
||||
* @version $Revision$
|
||||
* @author Benjamin Eberlei <kontakt@beberlei.de>
|
||||
* @author Roman Borschel <roman@code-factory.org>
|
||||
*/
|
||||
class PessimisticLockException extends ORMException
|
||||
{
|
||||
public static function lockFailed()
|
||||
{
|
||||
return new self("The pessimistic lock failed.");
|
||||
}
|
||||
}
|
@ -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) {
|
||||
|
40
lib/Doctrine/ORM/TransactionRequiredException.php
Normal file
40
lib/Doctrine/ORM/TransactionRequiredException.php
Normal file
@ -0,0 +1,40 @@
|
||||
<?php
|
||||
/*
|
||||
* $Id$
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*
|
||||
* This software consists of voluntary contributions made by many individuals
|
||||
* and is licensed under the LGPL. For more information, see
|
||||
* <http://www.doctrine-project.org>.
|
||||
*/
|
||||
|
||||
namespace Doctrine\ORM;
|
||||
|
||||
/**
|
||||
* Is thrown when a transaction is required for the current operation, but there is none open.
|
||||
*
|
||||
* @license http://www.opensource.org/licenses/lgpl-license.php LGPL
|
||||
* @link www.doctrine-project.com
|
||||
* @since 1.0
|
||||
* @version $Revision$
|
||||
* @author Benjamin Eberlei <kontakt@beberlei.de>
|
||||
* @author Roman Borschel <roman@code-factory.org>
|
||||
*/
|
||||
class TransactionRequiredException extends ORMException
|
||||
{
|
||||
static public function transactionRequired()
|
||||
{
|
||||
return new self('An open transaction is required for this operation.');
|
||||
}
|
||||
}
|
@ -1346,7 +1346,7 @@ class UnitOfWork implements PropertyChangedListener
|
||||
$entityVersion = $class->reflFields[$class->versionField]->getValue($entity);
|
||||
// Throw exception if versions dont match.
|
||||
if ($managedCopyVersion != $entityVersion) {
|
||||
throw OptimisticLockException::lockFailed($entity);
|
||||
throw OptimisticLockException::lockFailedVersionMissmatch($entityVersion, $managedCopyVersion);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1630,6 +1630,48 @@ class UnitOfWork implements PropertyChangedListener
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Acquire a lock on the given entity.
|
||||
*
|
||||
* @param object $entity
|
||||
* @param int $lockMode
|
||||
* @param int $lockVersion
|
||||
*/
|
||||
public function lock($entity, $lockMode, $lockVersion = null)
|
||||
{
|
||||
if ($this->getEntityState($entity) != self::STATE_MANAGED) {
|
||||
throw new \InvalidArgumentException("Entity is not MANAGED.");
|
||||
}
|
||||
|
||||
$entityName = get_class($entity);
|
||||
$class = $this->_em->getClassMetadata($entityName);
|
||||
|
||||
if ($lockMode == \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");
|
||||
});
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
122
tests/Doctrine/Tests/DBAL/Functional/WriteTest.php
Normal file
122
tests/Doctrine/Tests/DBAL/Functional/WriteTest.php
Normal file
@ -0,0 +1,122 @@
|
||||
<?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)));
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
177
tests/Doctrine/Tests/ORM/Functional/Locking/GearmanLockTest.php
Normal file
177
tests/Doctrine/Tests/ORM/Functional/Locking/GearmanLockTest.php
Normal file
@ -0,0 +1,177 @@
|
||||
<?php
|
||||
|
||||
namespace Doctrine\Tests\ORM\Functional\Locking;
|
||||
|
||||
use Doctrine\Tests\Models\CMS\CmsArticle,
|
||||
Doctrine\Tests\Models\CMS\CmsUser,
|
||||
Doctrine\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());
|
||||
}
|
||||
}
|
112
tests/Doctrine/Tests/ORM/Functional/Locking/LockAgentWorker.php
Normal file
112
tests/Doctrine/Tests/ORM/Functional/Locking/LockAgentWorker.php
Normal file
@ -0,0 +1,112 @@
|
||||
<?php
|
||||
|
||||
namespace Doctrine\Tests\ORM\Functional\Locking;
|
||||
|
||||
require_once __DIR__ . "/../../../TestInit.php";
|
||||
|
||||
class LockAgentWorker
|
||||
{
|
||||
private $em;
|
||||
|
||||
static public function run()
|
||||
{
|
||||
$lockAgent = new LockAgentWorker();
|
||||
|
||||
$worker = new \GearmanWorker();
|
||||
$worker->addServer();
|
||||
$worker->addFunction("findWithLock", array($lockAgent, "findWithLock"));
|
||||
$worker->addFunction("dqlWithLock", array($lockAgent, "dqlWithLock"));
|
||||
$worker->addFunction('lock', array($lockAgent, 'lock'));
|
||||
|
||||
while($worker->work()) {
|
||||
if ($worker->returnCode() != GEARMAN_SUCCESS) {
|
||||
echo "return_code: " . $worker->returnCode() . "\n";
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected function process($job, \Closure $do)
|
||||
{
|
||||
$fixture = $this->processWorkload($job);
|
||||
|
||||
$s = microtime(true);
|
||||
$this->em->beginTransaction();
|
||||
$do($fixture, $this->em);
|
||||
|
||||
sleep(1);
|
||||
$this->em->rollback();
|
||||
$this->em->clear();
|
||||
$this->em->close();
|
||||
$this->em->getConnection()->close();
|
||||
|
||||
return (microtime(true) - $s);
|
||||
}
|
||||
|
||||
public function findWithLock($job)
|
||||
{
|
||||
return $this->process($job, function($fixture, $em) {
|
||||
$entity = $em->find($fixture['entityName'], $fixture['entityId'], $fixture['lockMode']);
|
||||
});
|
||||
}
|
||||
|
||||
public function dqlWithLock($job)
|
||||
{
|
||||
return $this->process($job, function($fixture, $em) {
|
||||
/* @var $query Doctrine\ORM\Query */
|
||||
$query = $em->createQuery($fixture['dql']);
|
||||
$query->setLockMode($fixture['lockMode']);
|
||||
$query->setParameters($fixture['dqlParams']);
|
||||
$result = $query->getResult();
|
||||
});
|
||||
}
|
||||
|
||||
public function lock($job)
|
||||
{
|
||||
return $this->process($job, function($fixture, $em) {
|
||||
$entity = $em->find($fixture['entityName'], $fixture['entityId']);
|
||||
$em->lock($entity, $fixture['lockMode']);
|
||||
});
|
||||
}
|
||||
|
||||
protected function processWorkload($job)
|
||||
{
|
||||
echo "Received job: " . $job->handle() . " for function " . $job->functionName() . "\n";
|
||||
|
||||
$workload = $job->workload();
|
||||
$workload = unserialize($workload);
|
||||
|
||||
if (!isset($workload['conn']) || !is_array($workload['conn'])) {
|
||||
throw new \InvalidArgumentException("Missing Database parameters");
|
||||
}
|
||||
|
||||
$this->em = $this->createEntityManager($workload['conn']);
|
||||
|
||||
if (!isset($workload['fixture'])) {
|
||||
throw new \InvalidArgumentException("Missing Fixture parameters");
|
||||
}
|
||||
return $workload['fixture'];
|
||||
}
|
||||
|
||||
protected function createEntityManager($conn)
|
||||
{
|
||||
$config = new \Doctrine\ORM\Configuration();
|
||||
$config->setProxyDir(__DIR__ . '/../../../Proxies');
|
||||
$config->setProxyNamespace('MyProject\Proxies');
|
||||
$config->setAutoGenerateProxyClasses(true);
|
||||
|
||||
$annotDriver = $config->newDefaultAnnotationDriver(array(__DIR__ . '/../../../Models/'));
|
||||
$config->setMetadataDriverImpl($annotDriver);
|
||||
|
||||
$cache = new \Doctrine\Common\Cache\ArrayCache();
|
||||
$config->setMetadataCacheImpl($cache);
|
||||
$config->setQueryCacheImpl($cache);
|
||||
$config->setSQLLogger(new \Doctrine\DBAL\Logging\EchoSQLLogger());
|
||||
|
||||
$em = \Doctrine\ORM\EntityManager::create($conn, $config);
|
||||
|
||||
return $em;
|
||||
}
|
||||
}
|
||||
|
||||
LockAgentWorker::run();
|
157
tests/Doctrine/Tests/ORM/Functional/Locking/LockTest.php
Normal file
157
tests/Doctrine/Tests/ORM/Functional/Locking/LockTest.php
Normal file
@ -0,0 +1,157 @@
|
||||
<?php
|
||||
|
||||
namespace Doctrine\Tests\ORM\Functional\Locking;
|
||||
|
||||
use Doctrine\Tests\Models\CMS\CmsArticle,
|
||||
Doctrine\Tests\Models\CMS\CmsUser,
|
||||
Doctrine\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']);
|
||||
}
|
||||
}
|
207
tests/Doctrine/Tests/ORM/Functional/Ticket/DDC440Test.php
Normal file
207
tests/Doctrine/Tests/ORM/Functional/Ticket/DDC440Test.php
Normal file
@ -0,0 +1,207 @@
|
||||
<?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()
|
||||
{
|
||||
|
25
tests/README.markdown
Normal file
25
tests/README.markdown
Normal file
@ -0,0 +1,25 @@
|
||||
# Running the Doctrine 2 Testsuite
|
||||
|
||||
## Setting up a PHPUnit Configuration XML
|
||||
|
||||
..
|
||||
|
||||
## Testing Lock-Support
|
||||
|
||||
The Lock support in Doctrine 2 is tested using Gearman, which allows to run concurrent tasks in parallel.
|
||||
Install Gearman with PHP as follows:
|
||||
|
||||
1. Go to http://www.gearman.org and download the latest Gearman Server
|
||||
2. Compile it and then call ldconfig
|
||||
3. Start it up "gearmand -vvvv"
|
||||
4. Install pecl/gearman by calling "gearman-beta"
|
||||
|
||||
You can then go into tests/ and start up two workers:
|
||||
|
||||
php Doctrine/Tests/ORM/Functional/Locking/LockAgentWorker.php
|
||||
|
||||
Then run the locking test-suite:
|
||||
|
||||
phpunit --configuration <myconfig.xml> Doctrine/Tests/ORM/Functional/Locking/GearmanLockTest.php
|
||||
|
||||
This can run considerable time, because it is using sleep() to test for the timing ranges of locks.
|
Loading…
Reference in New Issue
Block a user