Skip to content

Commit 2fadbae

Browse files
committed
Optimize INSERT query to avoid an extra UPDATE
BaseEntityPersister can save an extra UPDATE query when it persists a self-referencing entity using an application generated identifier Fixes #7877
1 parent e9e012a commit 2fadbae

File tree

2 files changed

+154
-4
lines changed

2 files changed

+154
-4
lines changed

lib/Doctrine/ORM/Persisters/Entity/BasicEntityPersister.php

+21-4
Original file line numberDiff line numberDiff line change
@@ -614,7 +614,8 @@ protected function prepareUpdateData($entity)
614614
$versionField = $this->class->versionField;
615615
}
616616

617-
foreach ($uow->getEntityChangeSet($entity) as $field => $change) {
617+
$entityChangeSet = $uow->getEntityChangeSet($entity);
618+
foreach ($entityChangeSet as $field => $change) {
618619
if (isset($versionField) && $versionField == $field) {
619620
continue;
620621
}
@@ -650,9 +651,25 @@ protected function prepareUpdateData($entity)
650651
// The associated entity $newVal is not yet persisted, so we must
651652
// set $newVal = null, in order to insert a null value and schedule an
652653
// extra update on the UnitOfWork.
653-
$uow->scheduleExtraUpdate($entity, [$field => [null, $newVal]]);
654-
655-
$newVal = null;
654+
// This is only required if the associated entity is different from the current one,
655+
// and if the current entity relies on the database to generate its id
656+
if ($newVal !== $entity) {
657+
$scheduleExtraUpdate = true;
658+
} else {
659+
$identifiers = $this->class->getIdentifier();
660+
// Only single-column identifiers are supported
661+
if (1 === count($identifiers) && isset($entityChangeSet[$identifiers[0]])) {
662+
// Extra update is required if the current entity does not have yet a value for its identifier
663+
$scheduleExtraUpdate = ($entityChangeSet[$identifiers[0]][1] === null);
664+
} else {
665+
$scheduleExtraUpdate = true;
666+
}
667+
}
668+
669+
if ($scheduleExtraUpdate) {
670+
$uow->scheduleExtraUpdate($entity, [$field => [null, $newVal]]);
671+
$newVal = null;
672+
}
656673
}
657674
}
658675

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Doctrine\Tests\ORM\Functional;
6+
7+
use Doctrine\ORM\Annotation as ORM;
8+
use Doctrine\Tests\OrmFunctionalTestCase;
9+
10+
class SelfReferencingTest extends OrmFunctionalTestCase
11+
{
12+
protected function setUp() : void
13+
{
14+
parent::setUp();
15+
try {
16+
$classMetadatas = [
17+
$this->em->getClassMetadata(Issue7877ApplicationGenerated::class),
18+
$this->em->getClassMetadata(Issue7877DatabaseGenerated::class),
19+
];
20+
// We first drop the schema to avoid collision between tests
21+
$this->schemaTool->dropSchema($classMetadatas);
22+
$this->schemaTool->createSchema($classMetadatas);
23+
} catch (Exception $e) {
24+
}
25+
}
26+
27+
public function providerDifferentEntity()
28+
{
29+
yield [Issue7877ApplicationGenerated::class];
30+
yield [Issue7877DatabaseGenerated::class];
31+
}
32+
33+
/**
34+
* @dataProvider providerDifferentEntity
35+
*/
36+
public function testExtraUpdateWithDifferentEntities(string $class)
37+
{
38+
$count = count($this->sqlLoggerStack->queries);
39+
40+
$parent = new $class($parentId = 1);
41+
$this->em->persist($parent);
42+
43+
$child = new $class($childId = 2);
44+
$child->parent = $parent;
45+
$this->em->persist($child);
46+
47+
$this->em->flush();
48+
$this->assertCount($count + 4, $this->sqlLoggerStack->queries);
49+
50+
$this->em->clear();
51+
52+
$child = $this->em->find($class, $childId);
53+
$this->assertSame($parentId, $child->parent->id);
54+
}
55+
56+
public function testNoExtraUpdateWithApplicationGeneratedId()
57+
{
58+
$count = count($this->sqlLoggerStack->queries);
59+
60+
$entity = new Issue7877ApplicationGenerated($entityId = 1);
61+
$entity->parent = $entity;
62+
$this->em->persist($entity);
63+
64+
$this->em->flush();
65+
$this->assertCount($count + 3, $this->sqlLoggerStack->queries);
66+
67+
$this->em->clear();
68+
69+
$child = $this->em->find(Issue7877ApplicationGenerated::class, $entityId);
70+
$this->assertSame($entityId, $child->parent->id);
71+
}
72+
73+
public function textExtraUpdateWithDatabaseGeneratedId()
74+
{
75+
$count = count($this->sqlLoggerStack->queries);
76+
77+
$entity = new Issue7877DatabaseGenerated();
78+
$entity->parent = $entity;
79+
$this->em->persist($entity);
80+
81+
$this->em->flush();
82+
$this->assertCount($count + 4, $this->sqlLoggerStack->queries);
83+
$entityId = $entity->id;
84+
85+
$this->em->clear();
86+
87+
$child = $this->em->find(Issue7877DatabaseGenerated::class, $entityId);
88+
$this->assertSame($entityId, $child->paren->id);
89+
}
90+
}
91+
92+
/**
93+
* @ORM\Entity
94+
*/
95+
class Issue7877ApplicationGenerated
96+
{
97+
public function __construct(int $id)
98+
{
99+
$this->id = $id;
100+
}
101+
102+
/**
103+
* @ORM\Id
104+
* @ORM\Column(type="integer")
105+
* @ORM\GeneratedValue(strategy="NONE")
106+
*/
107+
public $id;
108+
109+
/**
110+
* @var self
111+
* @ORM\ManyToOne(targetEntity="Doctrine\Tests\ORM\Functional\Issue7877ApplicationGenerated")
112+
*/
113+
public $parent;
114+
}
115+
116+
/**
117+
* @ORM\Entity
118+
*/
119+
class Issue7877DatabaseGenerated
120+
{
121+
/**
122+
* @ORM\Id
123+
* @ORM\Column(type="integer")
124+
* @ORM\GeneratedValue(strategy="AUTO")
125+
*/
126+
public $id;
127+
128+
/**
129+
* @var self
130+
* @ORM\ManyToOne(targetEntity="Doctrine\Tests\ORM\Functional\Issue7877DatabaseGenerated")
131+
*/
132+
public $parent;
133+
}

0 commit comments

Comments
 (0)