Skip to content

Commit e9b6fd8

Browse files
committed
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.
1 parent 7ef4afc commit e9b6fd8

File tree

1 file changed

+158
-0
lines changed

1 file changed

+158
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Doctrine\Tests\ORM\Functional\Ticket;
6+
7+
use Doctrine\Common\Collections\ArrayCollection;
8+
use Doctrine\Common\Collections\Collection;
9+
use Doctrine\ORM\Mapping as ORM;
10+
use Doctrine\Tests\OrmFunctionalTestCase;
11+
12+
class GH5742Test extends OrmFunctionalTestCase
13+
{
14+
protected function setUp(): void
15+
{
16+
parent::setUp();
17+
18+
$this->createSchemaForModels(
19+
GH5742Person::class,
20+
GH5742Toothbrush::class,
21+
GH5742ToothpasteBrand::class
22+
);
23+
}
24+
25+
public function testUpdateOneToOneToNewEntityBeforePreviousEntityCanBeRemoved(): void
26+
{
27+
$person = new GH5742Person();
28+
$oldToothbrush = new GH5742Toothbrush();
29+
$person->toothbrush = $oldToothbrush;
30+
31+
$this->_em->persist($person);
32+
$this->_em->persist($oldToothbrush);
33+
$this->_em->flush();
34+
35+
$oldToothbrushId = $oldToothbrush->id;
36+
37+
$newToothbrush = new GH5742Toothbrush();
38+
$person->toothbrush = $newToothbrush;
39+
40+
$this->_em->remove($oldToothbrush);
41+
$this->_em->persist($newToothbrush);
42+
43+
// The flush operation will have to make sure the new toothbrush
44+
// has been written to the database
45+
// _before_ the person can be updated to refer to it.
46+
// Likewise, the update must have happened _before_ the old
47+
// toothbrush can be removed (non-nullable FK constraint).
48+
49+
$this->_em->flush();
50+
51+
$this->_em->clear();
52+
self::assertSame($newToothbrush->id, $this->_em->find(GH5742Person::class, $person->id)->toothbrush->id);
53+
self::assertNull($this->_em->find(GH5742Toothbrush::class, $oldToothbrushId));
54+
}
55+
56+
public function testManyToManyCollectionUpdateBeforeRemoval(): void
57+
{
58+
$person = new GH5742Person();
59+
$person->toothbrush = new GH5742Toothbrush(); // to satisfy not-null constraint
60+
$this->_em->persist($person);
61+
62+
$oldMice = new GH5742ToothpasteBrand();
63+
$this->_em->persist($oldMice);
64+
65+
$person->preferredBrands->set(1, $oldMice);
66+
$this->_em->flush();
67+
68+
$oldBrandId = $oldMice->id;
69+
70+
$newSpice = new GH5742ToothpasteBrand();
71+
$this->_em->persist($newSpice);
72+
73+
$person->preferredBrands->set(1, $newSpice);
74+
75+
$this->_em->remove($oldMice);
76+
77+
// The flush operation will have to make sure the new brand
78+
// has been written to the database _before_ it can be referred
79+
// to from the m2m join table.
80+
// Likewise, the old join table entry must have been removed
81+
// _before_ the old brand can be removed.
82+
83+
$this->_em->flush();
84+
85+
$this->_em->clear();
86+
self::assertCount(1, $this->_em->find(GH5742Person::class, $person->id)->preferredBrands);
87+
self::assertNull($this->_em->find(GH5742ToothpasteBrand::class, $oldBrandId));
88+
}
89+
}
90+
91+
/**
92+
* @ORM\Entity
93+
*/
94+
class GH5742Person
95+
{
96+
/**
97+
* @ORM\Id
98+
* @ORM\GeneratedValue(strategy="AUTO")
99+
* @ORM\Column(type="integer")
100+
*
101+
* @var int
102+
*/
103+
public $id;
104+
105+
/**
106+
* @ORM\OneToOne(targetEntity="GH5742Toothbrush", cascade={"persist"})
107+
* @ORM\JoinColumn(nullable=false)
108+
*
109+
* @var GH5742Toothbrush
110+
*/
111+
public $toothbrush;
112+
113+
/**
114+
* @ORM\ManyToMany(targetEntity="GH5742ToothpasteBrand")
115+
* @ORM\JoinTable(name="gh5742person_gh5742toothpastebrand",
116+
* joinColumns={@ORM\JoinColumn(name="person_id", referencedColumnName="id", onDelete="CASCADE")},
117+
* inverseJoinColumns={@ORM\JoinColumn(name="brand_id", referencedColumnName="id")}
118+
* )
119+
*
120+
* @var Collection<GH5742ToothpasteBrand>
121+
*/
122+
public $preferredBrands;
123+
124+
public function __construct()
125+
{
126+
$this->preferredBrands = new ArrayCollection();
127+
}
128+
}
129+
130+
/**
131+
* @ORM\Entity
132+
*/
133+
class GH5742Toothbrush
134+
{
135+
/**
136+
* @ORM\Id
137+
* @ORM\GeneratedValue(strategy="AUTO")
138+
* @ORM\Column(type="integer")
139+
*
140+
* @var int
141+
*/
142+
public $id;
143+
}
144+
145+
/**
146+
* @ORM\Entity
147+
*/
148+
class GH5742ToothpasteBrand
149+
{
150+
/**
151+
* @ORM\Id
152+
* @ORM\GeneratedValue(strategy="AUTO")
153+
* @ORM\Column(type="integer")
154+
*
155+
* @var int
156+
*/
157+
public $id;
158+
}

0 commit comments

Comments
 (0)