forked from doctrine/orm
-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add test to show why delete-before-insert may be challenging
There are a few requests (doctrine#5742, doctrine#5368, doctrine#5109, doctrine#6776) that ask to change the order of operations in the UnitOfWork to perform "deletes before inserts", or where such a switch appears to solve a reported problem. I don't want to say that this is not doable. But this PR at least adds two tricky examples where INSERTs need to be done before an UPDATE can refer to new database rows; and where the UPDATE needs to happen to release foreign key references to other entities before those can be DELETEd. So, at least as long as all operations of a certain type are to be executed in blocks, this example allows no other order of operations than the current one.
- Loading branch information
Showing
1 changed file
with
158 additions
and
0 deletions.
There are no files selected for viewing
158 changes: 158 additions & 0 deletions
158
tests/Doctrine/Tests/ORM/Functional/Ticket/GH5742Test.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,158 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
namespace Doctrine\Tests\ORM\Functional\Ticket; | ||
|
||
use Doctrine\Common\Collections\ArrayCollection; | ||
use Doctrine\Common\Collections\Collection; | ||
use Doctrine\ORM\Mapping as ORM; | ||
use Doctrine\Tests\OrmFunctionalTestCase; | ||
|
||
class GH5742Test extends OrmFunctionalTestCase | ||
{ | ||
protected function setUp(): void | ||
{ | ||
parent::setUp(); | ||
|
||
$this->createSchemaForModels( | ||
GH5742Person::class, | ||
GH5742Toothbrush::class, | ||
GH5742ToothpasteBrand::class | ||
); | ||
} | ||
|
||
public function testUpdateOneToOneToNewEntityBeforePreviousEntityCanBeRemoved(): void | ||
{ | ||
$person = new GH5742Person(); | ||
$oldToothbrush = new GH5742Toothbrush(); | ||
$person->toothbrush = $oldToothbrush; | ||
|
||
$this->_em->persist($person); | ||
$this->_em->persist($oldToothbrush); | ||
$this->_em->flush(); | ||
|
||
$oldToothbrushId = $oldToothbrush->id; | ||
|
||
$newToothbrush = new GH5742Toothbrush(); | ||
$person->toothbrush = $newToothbrush; | ||
|
||
$this->_em->remove($oldToothbrush); | ||
$this->_em->persist($newToothbrush); | ||
|
||
// The flush operation will have to make sure the new toothbrush | ||
// has been written to the database | ||
// _before_ the person can be updated to refer to it. | ||
// Likewise, the update must have happened _before_ the old | ||
// toothbrush can be removed (non-nullable FK constraint). | ||
|
||
$this->_em->flush(); | ||
|
||
$this->_em->clear(); | ||
self::assertSame($newToothbrush->id, $this->_em->find(GH5742Person::class, $person->id)->toothbrush->id); | ||
self::assertNull($this->_em->find(GH5742Toothbrush::class, $oldToothbrushId)); | ||
} | ||
|
||
public function testManyToManyCollectionUpdateBeforeRemoval(): void | ||
{ | ||
$person = new GH5742Person(); | ||
$person->toothbrush = new GH5742Toothbrush(); // to satisfy not-null constraint | ||
$this->_em->persist($person); | ||
|
||
$oldMice = new GH5742ToothpasteBrand(); | ||
$this->_em->persist($oldMice); | ||
|
||
$person->preferredBrands->set(1, $oldMice); | ||
$this->_em->flush(); | ||
|
||
$oldBrandId = $oldMice->id; | ||
|
||
$newSpice = new GH5742ToothpasteBrand(); | ||
$this->_em->persist($newSpice); | ||
|
||
$person->preferredBrands->set(1, $newSpice); | ||
|
||
$this->_em->remove($oldMice); | ||
|
||
// The flush operation will have to make sure the new brand | ||
// has been written to the database _before_ it can be referred | ||
// to from the m2m join table. | ||
// Likewise, the old join table entry must have been removed | ||
// _before_ the old brand can be removed. | ||
|
||
$this->_em->flush(); | ||
|
||
$this->_em->clear(); | ||
self::assertCount(1, $this->_em->find(GH5742Person::class, $person->id)->preferredBrands); | ||
self::assertNull($this->_em->find(GH5742ToothpasteBrand::class, $oldBrandId)); | ||
} | ||
} | ||
|
||
/** | ||
* @ORM\Entity | ||
*/ | ||
class GH5742Person | ||
{ | ||
/** | ||
* @ORM\Id | ||
* @ORM\GeneratedValue(strategy="AUTO") | ||
* @ORM\Column(type="integer") | ||
* | ||
* @var int | ||
*/ | ||
public $id; | ||
|
||
/** | ||
* @ORM\OneToOne(targetEntity="GH5742Toothbrush", cascade={"persist"}) | ||
* @ORM\JoinColumn(nullable=false) | ||
* | ||
* @var GH5742Toothbrush | ||
*/ | ||
public $toothbrush; | ||
|
||
/** | ||
* @ORM\ManyToMany(targetEntity="GH5742ToothpasteBrand") | ||
* @ORM\JoinTable(name="gh5742person_gh5742toothpastebrand", | ||
* joinColumns={@ORM\JoinColumn(name="person_id", referencedColumnName="id", onDelete="CASCADE")}, | ||
* inverseJoinColumns={@ORM\JoinColumn(name="brand_id", referencedColumnName="id")} | ||
* ) | ||
* | ||
* @var Collection<GH5742ToothpasteBrand> | ||
*/ | ||
public $preferredBrands; | ||
|
||
public function __construct() | ||
{ | ||
$this->preferredBrands = new ArrayCollection(); | ||
} | ||
} | ||
|
||
/** | ||
* @ORM\Entity | ||
*/ | ||
class GH5742Toothbrush | ||
{ | ||
/** | ||
* @ORM\Id | ||
* @ORM\GeneratedValue(strategy="AUTO") | ||
* @ORM\Column(type="integer") | ||
* | ||
* @var int | ||
*/ | ||
public $id; | ||
} | ||
|
||
/** | ||
* @ORM\Entity | ||
*/ | ||
class GH5742ToothpasteBrand | ||
{ | ||
/** | ||
* @ORM\Id | ||
* @ORM\GeneratedValue(strategy="AUTO") | ||
* @ORM\Column(type="integer") | ||
* | ||
* @var int | ||
*/ | ||
public $id; | ||
} |