-
Notifications
You must be signed in to change notification settings - Fork 2.5k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Mitigate problems with
EntityManager::flush()
reentrance since 2.16…
….0 (Take 2) The changes from #10547, which landed in 2.16.0, cause problems for users calling `EntityManager::flush()` from within `postPersist` event listeners. * When `UnitOfWork::commit()` is re-entered, the "inner" (reentrant) call will start working through all changesets. Eventually, it finishes with all insertions being performed and `UoW::$entityInsertions` being empty. After return, the entity insertion order, an array computed at the beginning of `UnitOfWork::executeInserts()`, still contains entities that now have been processed already. This leads to a strange-looking SQL error where the number of parameters used does not match the number of parameters bound. This has been reported as #10869. * The fixes made to the commit order computation may lead to a different entity insertion order than previously. `postPersist` listener code may be affected by this when accessing generated IDs for other entities than the one the event has been dispatched for. This ID may not yet be available when the insertion order is different from the one that was used before 2.16. This has been mentioned in #10906 (comment). This PR suggests to address both issues by dispatching the `postPersist` event only after _all_ new entities have their rows inserted into the database. Likewise, dispatch `postRemove` only after _all_ deletions have been executed. This solves the first issue because the sequence of insertions or deletions has been processed completely _before_ we start calling event listeners. This way, potential changes made by listeners will no longer be relevant. Regarding the second issue, I think deferring `postPersist` a bit until _all_ entities have been inserted does not violate any promises given, hence is not a BC break. In 2.15, this event was raised after all insertions _for a particular class_ had been processed - so, it was never an "immediate" event for every single entity. #10547 moved the event handling to directly after every single insertion. Now, this PR moves it back a bit to after _all_ insertions.
- Loading branch information
Showing
4 changed files
with
225 additions
and
11 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
76 changes: 76 additions & 0 deletions
76
tests/Doctrine/Tests/ORM/Functional/Ticket/GH10869Test.php
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,76 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
namespace Doctrine\Tests\ORM\Functional\Ticket; | ||
|
||
use Doctrine\ORM\Event\PostPersistEventArgs; | ||
use Doctrine\ORM\Events; | ||
use Doctrine\ORM\Mapping as ORM; | ||
use Doctrine\Tests\OrmFunctionalTestCase; | ||
|
||
class GH10869Test extends OrmFunctionalTestCase | ||
{ | ||
protected function setUp(): void | ||
{ | ||
parent::setUp(); | ||
|
||
$this->setUpEntitySchema([ | ||
GH10869Entity::class, | ||
]); | ||
} | ||
|
||
public function testPostPersistListenerUpdatingObjectFieldWhileOtherInsertPending(): void | ||
{ | ||
$entity1 = new GH10869Entity(); | ||
$this->_em->persist($entity1); | ||
|
||
$entity2 = new GH10869Entity(); | ||
$this->_em->persist($entity2); | ||
|
||
$this->_em->getEventManager()->addEventListener(Events::postPersist, new class { | ||
public function postPersist(PostPersistEventArgs $args): void | ||
{ | ||
$object = $args->getObject(); | ||
|
||
$objectManager = $args->getObjectManager(); | ||
$object->field = 'test ' . $object->id; | ||
$objectManager->flush(); | ||
} | ||
}); | ||
|
||
$this->_em->flush(); | ||
$this->_em->clear(); | ||
|
||
self::assertSame('test ' . $entity1->id, $entity1->field); | ||
self::assertSame('test ' . $entity2->id, $entity2->field); | ||
|
||
$entity1Reloaded = $this->_em->find(GH10869Entity::class, $entity1->id); | ||
self::assertSame($entity1->field, $entity1Reloaded->field); | ||
|
||
$entity2Reloaded = $this->_em->find(GH10869Entity::class, $entity2->id); | ||
self::assertSame($entity2->field, $entity2Reloaded->field); | ||
} | ||
} | ||
|
||
/** | ||
* @ORM\Entity | ||
*/ | ||
class GH10869Entity | ||
{ | ||
/** | ||
* @ORM\Id | ||
* @ORM\GeneratedValue | ||
* @ORM\Column(type="integer") | ||
* | ||
* @var ?int | ||
*/ | ||
public $id; | ||
|
||
/** | ||
* @ORM\Column(type="text", nullable=true) | ||
* | ||
* @var ?string | ||
*/ | ||
public $field; | ||
} |