Skip to content

Commit

Permalink
Add lockMode to EntityManager#refresh() (#10040)
Browse files Browse the repository at this point in the history
  • Loading branch information
michnovka authored Nov 1, 2022
1 parent 75340b6 commit 25ce9b9
Show file tree
Hide file tree
Showing 7 changed files with 110 additions and 13 deletions.
6 changes: 5 additions & 1 deletion docs/en/reference/transactions-and-concurrency.rst
Original file line number Diff line number Diff line change
Expand Up @@ -412,7 +412,7 @@ Doctrine ORM currently supports two pessimistic lock modes:
locks other concurrent requests that attempt to update or lock rows
in write mode.

You can use pessimistic locks in three different scenarios:
You can use pessimistic locks in four different scenarios:


1. Using
Expand All @@ -424,6 +424,10 @@ You can use pessimistic locks in three different scenarios:
or
``EntityManager#lock($entity, \Doctrine\DBAL\LockMode::PESSIMISTIC_READ)``
3. Using
``EntityManager#refresh($entity, \Doctrine\DBAL\LockMode::PESSIMISTIC_WRITE)``
or
``EntityManager#refresh($entity, \Doctrine\DBAL\LockMode::PESSIMISTIC_READ)``
4. Using
``Query#setLockMode(\Doctrine\DBAL\LockMode::PESSIMISTIC_WRITE)``
or
``Query#setLockMode(\Doctrine\DBAL\LockMode::PESSIMISTIC_READ)``
16 changes: 16 additions & 0 deletions lib/Doctrine/ORM/Decorator/EntityManagerDecorator.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
use Doctrine\ORM\Query\ResultSetMapping;
use Doctrine\Persistence\ObjectManagerDecorator;

use function func_get_arg;
use function func_num_args;
use function get_debug_type;
use function method_exists;
use function sprintf;
Expand Down Expand Up @@ -211,6 +213,20 @@ public function flush($entity = null)
$this->wrapped->flush($entity);
}

/**
* {@inheritdoc}
*/
public function refresh($object)
{
$lockMode = null;

if (func_num_args() > 1) {
$lockMode = func_get_arg(1);
}

$this->wrapped->refresh($object, $lockMode);
}

/**
* {@inheritdoc}
*/
Expand Down
8 changes: 5 additions & 3 deletions lib/Doctrine/ORM/EntityManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -693,22 +693,24 @@ public function remove($entity)
* Refreshes the persistent state of an entity from the database,
* overriding any local changes that have not yet been persisted.
*
* @param object $entity The entity to refresh.
* @param object $entity The entity to refresh
* @psalm-param LockMode::*|null $lockMode
*
* @return void
*
* @throws ORMInvalidArgumentException
* @throws ORMException
* @throws TransactionRequiredException
*/
public function refresh($entity)
public function refresh($entity, ?int $lockMode = null)
{
if (! is_object($entity)) {
throw ORMInvalidArgumentException::invalidObject('EntityManager#refresh()', $entity);
}

$this->errorIfClosed();

$this->unitOfWork->refresh($entity);
$this->unitOfWork->refresh($entity, $lockMode);
}

/**
Expand Down
1 change: 1 addition & 0 deletions lib/Doctrine/ORM/EntityManagerInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
*
* @method Mapping\ClassMetadataFactory getMetadataFactory()
* @method mixed wrapInTransaction(callable $func)
* @method void refresh(object $object, ?int $lockMode = null)
*/
interface EntityManagerInterface extends ObjectManager
{
Expand Down
37 changes: 29 additions & 8 deletions lib/Doctrine/ORM/UnitOfWork.php
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,8 @@
use function assert;
use function count;
use function current;
use function func_get_arg;
use function func_num_args;
use function get_class;
use function get_debug_type;
use function implode;
Expand Down Expand Up @@ -2192,29 +2194,46 @@ private function doDetach(
* Refreshes the state of the given entity from the database, overwriting
* any local, unpersisted changes.
*
* @param object $entity The entity to refresh.
* @param object $entity The entity to refresh
*
* @return void
*
* @throws InvalidArgumentException If the entity is not MANAGED.
* @throws TransactionRequiredException
*/
public function refresh($entity)
{
$visited = [];

$this->doRefresh($entity, $visited);
$lockMode = null;

if (func_num_args() > 1) {
$lockMode = func_get_arg(1);
}

$this->doRefresh($entity, $visited, $lockMode);
}

/**
* Executes a refresh operation on an entity.
*
* @param object $entity The entity to refresh.
* @psalm-param array<int, object> $visited The already visited entities during cascades.
* @psalm-param LockMode::*|null $lockMode
*
* @throws ORMInvalidArgumentException If the entity is not MANAGED.
* @throws TransactionRequiredException
*/
private function doRefresh($entity, array &$visited): void
private function doRefresh($entity, array &$visited, ?int $lockMode = null): void
{
switch (true) {
case $lockMode === LockMode::PESSIMISTIC_READ:
case $lockMode === LockMode::PESSIMISTIC_WRITE:
if (! $this->em->getConnection()->isTransactionActive()) {
throw TransactionRequiredException::transactionRequired();
}
}

$oid = spl_object_id($entity);

if (isset($visited[$oid])) {
Expand All @@ -2231,19 +2250,21 @@ private function doRefresh($entity, array &$visited): void

$this->getEntityPersister($class->name)->refresh(
array_combine($class->getIdentifierFieldNames(), $this->entityIdentifiers[$oid]),
$entity
$entity,
$lockMode
);

$this->cascadeRefresh($entity, $visited);
$this->cascadeRefresh($entity, $visited, $lockMode);
}

/**
* Cascades a refresh operation to associated entities.
*
* @param object $entity
* @psalm-param array<int, object> $visited
* @psalm-param LockMode::*|null $lockMode
*/
private function cascadeRefresh($entity, array &$visited): void
private function cascadeRefresh($entity, array &$visited, ?int $lockMode = null): void
{
$class = $this->em->getClassMetadata(get_class($entity));

Expand All @@ -2266,13 +2287,13 @@ static function ($assoc) {
case $relatedEntities instanceof Collection:
case is_array($relatedEntities):
foreach ($relatedEntities as $relatedEntity) {
$this->doRefresh($relatedEntity, $visited);
$this->doRefresh($relatedEntity, $visited, $lockMode);
}

break;

case $relatedEntities !== null:
$this->doRefresh($relatedEntities, $visited);
$this->doRefresh($relatedEntities, $visited, $lockMode);
break;

default:
Expand Down
3 changes: 2 additions & 1 deletion psalm-baseline.xml
Original file line number Diff line number Diff line change
Expand Up @@ -269,9 +269,10 @@
<MoreSpecificImplementedParamType occurrences="1">
<code>$className</code>
</MoreSpecificImplementedParamType>
<TooManyArguments occurrences="2">
<TooManyArguments occurrences="3">
<code>find</code>
<code>flush</code>
<code>refresh</code>
</TooManyArguments>
</file>
<file src="lib/Doctrine/ORM/EntityManager.php">
Expand Down
52 changes: 52 additions & 0 deletions tests/Doctrine/Tests/ORM/Functional/Locking/LockTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,23 @@ public function testLockPessimisticWriteNoTransactionThrowsException(): void
$this->_em->lock($article, LockMode::PESSIMISTIC_WRITE);
}

/**
* @group locking
*/
public function testRefreshWithLockPessimisticWriteNoTransactionThrowsException(): void
{
$article = new CmsArticle();
$article->text = 'my article';
$article->topic = 'Hello';

$this->_em->persist($article);
$this->_em->flush();

$this->expectException(TransactionRequiredException::class);

$this->_em->refresh($article, LockMode::PESSIMISTIC_WRITE);
}

/**
* @group DDC-178
* @group locking
Expand Down Expand Up @@ -180,6 +197,41 @@ public function testLockPessimisticWrite(): void
self::assertStringContainsString($writeLockSql, $lastLoggedQuery);
}

/**
* @group locking
*/
public function testRefreshWithLockPessimisticWrite(): void
{
$writeLockSql = $this->_em->getConnection()->getDatabasePlatform()->getWriteLockSQL();

if (! $writeLockSql) {
self::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();
try {
$this->_em->refresh($article, LockMode::PESSIMISTIC_WRITE);
$this->_em->commit();
} catch (Exception $e) {
$this->_em->rollback();
}

$lastLoggedQuery = $this->getLastLoggedQuery()['sql'];
// DBAL 2 logs a commit as last query.
if ($lastLoggedQuery === '"COMMIT"') {
$lastLoggedQuery = $this->getLastLoggedQuery(1)['sql'];
}

self::assertStringContainsString($writeLockSql, $lastLoggedQuery);
}

/** @group DDC-178 */
public function testLockPessimisticRead(): void
{
Expand Down

0 comments on commit 25ce9b9

Please sign in to comment.