Skip to content

Commit

Permalink
[DDC-178] First approach to Locking support
Browse files Browse the repository at this point in the history
  • Loading branch information
beberlei committed Apr 8, 2010
1 parent 5381e3d commit e6a44b1
Show file tree
Hide file tree
Showing 20 changed files with 639 additions and 14 deletions.
42 changes: 42 additions & 0 deletions lib/Doctrine/DBAL/LockMode.php
@@ -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() { }
}
39 changes: 38 additions & 1 deletion lib/Doctrine/DBAL/Platforms/AbstractPlatform.php
Expand Up @@ -488,11 +488,48 @@ public function getCosExpression($value)
return 'COS(' . $value . ')'; return 'COS(' . $value . ')';
} }


public function getForUpdateSql() public function getForUpdateSQL()
{ {
return 'FOR UPDATE'; 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) public function getDropDatabaseSQL($database)
{ {
return 'DROP DATABASE ' . $database; return 'DROP DATABASE ' . $database;
Expand Down
28 changes: 28 additions & 0 deletions lib/Doctrine/DBAL/Platforms/MsSqlPlatform.php
Expand Up @@ -483,4 +483,32 @@ public function getTruncateTableSQL($tableName, $cascade = false)
{ {
return 'TRUNCATE TABLE '.$tableName; 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;
}
}
} }
5 changes: 5 additions & 0 deletions lib/Doctrine/DBAL/Platforms/MySqlPlatform.php
Expand Up @@ -666,4 +666,9 @@ public function createsExplicitIndexForForeignKeys()
{ {
return true; return true;
} }

public function getReadLockSQL()
{
return 'LOCK IN SHARE MODE';
}
} }
5 changes: 5 additions & 0 deletions lib/Doctrine/DBAL/Platforms/PostgreSqlPlatform.php
Expand Up @@ -637,4 +637,9 @@ public function getTruncateTableSQL($tableName, $cascade = false)
{ {
return 'TRUNCATE '.$tableName.' '.($cascade)?'CASCADE':''; return 'TRUNCATE '.$tableName.' '.($cascade)?'CASCADE':'';
} }

public function getReadLockSQL()
{
return 'FOR SHARE';
}
} }
5 changes: 5 additions & 0 deletions lib/Doctrine/DBAL/Platforms/SqlitePlatform.php
Expand Up @@ -428,4 +428,9 @@ static public function udfLocate($str, $substr, $offset = 0)
} }
return 0; return 0;
} }

public function getForUpdateSql()
{
return '';
}
} }
20 changes: 18 additions & 2 deletions lib/Doctrine/ORM/EntityManager.php
Expand Up @@ -288,11 +288,13 @@ public function flush()
* *
* @param string $entityName * @param string $entityName
* @param mixed $identifier * @param mixed $identifier
* @param int $lockMode
* @param int $lockVersion
* @return object * @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);
} }


/** /**
Expand Down Expand Up @@ -447,6 +449,20 @@ public function copy($entity, $deep = false)
throw new \BadMethodCallException("Not implemented."); throw new \BadMethodCallException("Not implemented.");
} }


/**
* Acquire a lock on the given entity.
*
* @param object $entity
* @param int $lockMode
* @param int $lockVersion
* @throws OptimisticLockException
* @throws PessimisticLockException
*/
public function lock($entity, $lockMode, $lockVersion)
{
$this->_unitOfWork->lock($entity, $lockMode, $lockVersion);
}

/** /**
* Gets the repository for an entity class. * Gets the repository for an entity class.
* *
Expand Down
30 changes: 26 additions & 4 deletions lib/Doctrine/ORM/EntityRepository.php
Expand Up @@ -92,23 +92,45 @@ public function clear()
* Finds an entity by its primary key / identifier. * Finds an entity by its primary key / identifier.
* *
* @param $id The identifier. * @param $id The identifier.
* @param int $hydrationMode The hydration mode to use. * @param int $lockMode
* @param int $lockVersion
* @return object The entity. * @return object The entity.
*/ */
public function find($id) public function find($id, $lockMode = LockMode::NONE, $lockVersion = null)
{ {
// Check identity map first // Check identity map first
if ($entity = $this->_em->getUnitOfWork()->tryGetById($id, $this->_class->rootEntityName)) { if ($entity = $this->_em->getUnitOfWork()->tryGetById($id, $this->_class->rootEntityName)) {
if ($lockMode != LockMode::NONE) {
$this->_em->lock($entity, $lockMode, $lockVersion);
}

return $entity; // Hit! return $entity; // Hit!
} }


if ( ! is_array($id) || count($id) <= 1) { 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); $value = is_array($id) ? array_values($id) : array($id);
$id = array_combine($this->_class->identifier, $value); $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);
}
} }


/** /**
Expand Down
37 changes: 37 additions & 0 deletions lib/Doctrine/ORM/LockMode.php
@@ -0,0 +1,37 @@
<?php
/*
* $Id$
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* This software consists of voluntary contributions made by many individuals
* and is licensed under the LGPL. For more information, see
* <http://www.doctrine-project.org>.
*/

namespace Doctrine\ORM;

/**
* Contains all ORM LockModes
*
* @license http://www.opensource.org/licenses/lgpl-license.php LGPL
* @link www.doctrine-project.com
* @since 1.0
* @version $Revision$
* @author Benjamin Eberlei <kontakt@beberlei.de>
* @author Roman Borschel <roman@code-factory.org>
*/
class LockMode extends \Doctrine\DBAL\LockMode
{

}
5 changes: 5 additions & 0 deletions lib/Doctrine/ORM/OptimisticLockException.php
Expand Up @@ -36,4 +36,9 @@ public static function lockFailed()
{ {
return new self("The optimistic lock failed."); return new self("The optimistic lock failed.");
} }

public static function notVersioned($className)
{
return new self("Cannot obtain optimistic lock on unversioned entity ".$className);
}
} }
2 changes: 1 addition & 1 deletion lib/Doctrine/ORM/Persisters/JoinedSubclassPersister.php
Expand Up @@ -235,7 +235,7 @@ public function delete($entity)
/** /**
* {@inheritdoc} * {@inheritdoc}
*/ */
protected function _getSelectEntitiesSQL(array &$criteria, $assoc = null, $orderBy = null) protected function _getSelectEntitiesSQL(array &$criteria, $assoc = null, $orderBy = null, $lockMode = 0)
{ {
$idColumns = $this->_class->getIdentifierColumnNames(); $idColumns = $this->_class->getIdentifierColumnNames();
$baseTableAlias = $this->_getSQLTableAlias($this->_class); $baseTableAlias = $this->_getSQLTableAlias($this->_class);
Expand Down
58 changes: 54 additions & 4 deletions lib/Doctrine/ORM/Persisters/StandardEntityPersister.php
Expand Up @@ -423,11 +423,12 @@ public function getOwningTable($fieldName)
* a new entity is created. * a new entity is created.
* @param $assoc The association that connects the entity to load to another entity, if any. * @param $assoc The association that connects the entity to load to another entity, if any.
* @param array $hints Hints for entity creation. * @param array $hints Hints for entity creation.
* @param int $lockMode
* @return The loaded entity instance or NULL if the entity/the data can not be found. * @return The loaded entity instance or NULL if the entity/the data can not be found.
*/ */
public function load(array $criteria, $entity = null, $assoc = null, array $hints = array()) public function load(array $criteria, $entity = null, $assoc = null, array $hints = array(), $lockMode = 0)
{ {
$sql = $this->_getSelectEntitiesSQL($criteria, $assoc); $sql = $this->_getSelectEntitiesSQL($criteria, $assoc, null, $lockMode);
$params = array_values($criteria); $params = array_values($criteria);
$stmt = $this->_conn->executeQuery($sql, $params); $stmt = $this->_conn->executeQuery($sql, $params);
$result = $stmt->fetch(PDO::FETCH_ASSOC); $result = $stmt->fetch(PDO::FETCH_ASSOC);
Expand Down Expand Up @@ -641,9 +642,10 @@ protected function _processSQLResult(array $sqlResult)
* @param array $criteria * @param array $criteria
* @param AssociationMapping $assoc * @param AssociationMapping $assoc
* @param string $orderBy * @param string $orderBy
* @param int $lockMode
* @return string * @return string
*/ */
protected function _getSelectEntitiesSQL(array &$criteria, $assoc = null, $orderBy = null) protected function _getSelectEntitiesSQL(array &$criteria, $assoc = null, $orderBy = null, $lockMode = 0)
{ {
// Construct WHERE conditions // Construct WHERE conditions
$conditionSql = ''; $conditionSql = '';
Expand Down Expand Up @@ -671,10 +673,17 @@ protected function _getSelectEntitiesSQL(array &$criteria, $assoc = null, $order
); );
} }


$lockSql = '';
if ($lockMode == \Doctrine\ORM\LockMode::PESSIMISTIC_READ) {
$lockSql = ' ' . $this->_platform->getReadLockSql();
} else if ($lockMode == \Doctrine\ORM\LockMode::PESSIMISTIC_WRITE) {
$lockSql = ' ' . $this->_platform->getWriteLockSql();
}

return 'SELECT ' . $this->_getSelectColumnListSQL() return 'SELECT ' . $this->_getSelectColumnListSQL()
. ' FROM ' . $this->_class->getQuotedTableName($this->_platform) . ' ' . ' FROM ' . $this->_class->getQuotedTableName($this->_platform) . ' '
. $this->_getSQLTableAlias($this->_class) . $this->_getSQLTableAlias($this->_class)
. ($conditionSql ? ' WHERE ' . $conditionSql : '') . $orderBySql; . ($conditionSql ? ' WHERE ' . $conditionSql : '') . $orderBySql . $lockSql;
} }


/** /**
Expand Down Expand Up @@ -912,4 +921,45 @@ protected function _getSQLTableAlias(ClassMetadata $class)


return $tableAlias; return $tableAlias;
} }

/**
* Lock all rows of this entity matching the given criteria with the specified pessimistic lock mode
*
* @param array $criteria
* @param int $lockMode
* @return void
*/
public function lock(array $criteria, $lockMode)
{
// @todo Extract method to remove duplicate code from _getSelectEntitiesSQL()?
$conditionSql = '';
foreach ($criteria as $field => $value) {
if ($conditionSql != '') {
$conditionSql .= ' AND ';
}

if (isset($this->_class->columnNames[$field])) {
$conditionSql .= $this->_class->getQuotedColumnName($field, $this->_platform);
} else if (isset($this->_class->fieldNames[$field])) {
$conditionSql .= $this->_class->getQuotedColumnName($this->_class->fieldNames[$field], $this->_platform);
} else if ($assoc !== null) {
$conditionSql .= $field;
} else {
throw ORMException::unrecognizedField($field);
}
$conditionSql .= ' = ?';
}

if ($lockMode == \Doctrine\ORM\LockMode::PESSIMISTIC_READ) {
$lockSql = $this->_platform->getReadLockSql();
} else if ($lockMode == \Doctrine\ORM\LockMode::PESSIMISTIC_WRITE) {
$lockSql = $this->_platform->getWriteLockSql();
}

$sql = 'SELECT 1 FROM ' . $this->_class->getQuotedTableName($this->_platform) . ' '
. $this->_getSQLTableAlias($this->_class)
. ($conditionSql ? ' WHERE ' . $conditionSql : '') . ' ' . $lockSql;
$params = array_values($criteria);
$this->_conn->executeQuery($query, $params);
}
} }

0 comments on commit e6a44b1

Please sign in to comment.