-
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.
Make it possible to have non-NULLable self-referencing associations w…
…hen using application-provided IDs This change improves scheduling of extra updates in the `BasicEntityPersister`. Extra updates can be avoided when * the referred-to entity has already been inserted during the current insert batch/transaction * we have a self-referencing entity with application-provided ID values (the `NONE` generator strategy). As a corollary, with this change applications that provide their own IDs can define self-referencing associations as not NULLable. I am considering this a bugfix since the ORM previously executed additional queries that were not strictly necessary, and that required users to work with NULLable columns where conceptually a non-NULLable column would be valid and more expressive. One caveat, though: In the absence of entity-level commit ordering (#10547), it is not guaranteed that entities with self-references (at the class level) will be inserted in a suitable order. The order depends on the sequence in which the entities were added with `persist()`. Fixes #7877, closes #7882. Co-authored-by: Sylvain Fabre <[email protected]>
- Loading branch information
Showing
2 changed files
with
176 additions
and
7 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,135 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
namespace Doctrine\Tests\ORM\Functional; | ||
|
||
use Doctrine\ORM\Mapping as ORM; | ||
use Doctrine\Tests\OrmFunctionalTestCase; | ||
|
||
use function uniqid; | ||
|
||
/** | ||
* @group GH7877 | ||
*/ | ||
class GH7877Test extends OrmFunctionalTestCase | ||
{ | ||
protected function setUp(): void | ||
{ | ||
parent::setUp(); | ||
|
||
$this->createSchemaForModels( | ||
GH7877ApplicationGeneratedIdEntity::class, | ||
GH7877EntityWithNullableAssociation::class | ||
); | ||
} | ||
|
||
public function testSelfReferenceWithApplicationGeneratedIdMayBeNotNullable(): void | ||
{ | ||
$entity = new GH7877ApplicationGeneratedIdEntity(); | ||
$entity->parent = $entity; | ||
|
||
$this->expectNotToPerformAssertions(); | ||
|
||
$this->_em->persist($entity); | ||
$this->_em->flush(); | ||
} | ||
|
||
public function testCrossReferenceWithApplicationGeneratedIdMayBeNotNullable(): void | ||
{ | ||
$entity1 = new GH7877ApplicationGeneratedIdEntity(); | ||
$entity1->parent = $entity1; | ||
$entity2 = new GH7877ApplicationGeneratedIdEntity(); | ||
$entity2->parent = $entity1; | ||
|
||
$this->expectNotToPerformAssertions(); | ||
|
||
// As long as we do not have entity-level commit order computation | ||
// (see https://github.com/doctrine/orm/pull/10547), | ||
// this only works when the UoW processes $entity1 before $entity2, | ||
// so that the foreign key constraint E2 -> E1 can be satisfied. | ||
|
||
$this->_em->persist($entity1); | ||
$this->_em->persist($entity2); | ||
$this->_em->flush(); | ||
} | ||
|
||
public function testNullableForeignKeysMakeInsertOrderLessRelevant(): void | ||
{ | ||
$entity1 = new GH7877EntityWithNullableAssociation(); | ||
$entity1->parent = $entity1; | ||
$entity2 = new GH7877EntityWithNullableAssociation(); | ||
$entity2->parent = $entity1; | ||
|
||
$this->expectNotToPerformAssertions(); | ||
|
||
// In contrast to the previous test, this case demonstrates that with NULLable | ||
// associations, even without entity-level commit order computation | ||
// (see https://github.com/doctrine/orm/pull/10547), we can get away with an | ||
// insertion order of E2 before E1. That is because the UoW will schedule an extra | ||
// update that saves the day - the foreign key reference will established only after | ||
// all insertions have been performed. | ||
|
||
$this->_em->persist($entity2); | ||
$this->_em->persist($entity1); | ||
$this->_em->flush(); | ||
} | ||
} | ||
|
||
/** | ||
* @ORM\Entity | ||
*/ | ||
class GH7877ApplicationGeneratedIdEntity | ||
{ | ||
/** | ||
* @ORM\Id | ||
* @ORM\Column(type="string") | ||
* @ORM\GeneratedValue(strategy="NONE") | ||
* | ||
* @var string | ||
*/ | ||
public $id; | ||
|
||
/** | ||
* (!) Note this uses "nullable=false" | ||
* | ||
* @ORM\ManyToOne(targetEntity="GH7877ApplicationGeneratedIdEntity") | ||
* @ORM\JoinColumn(name="parent_id", referencedColumnName="id", nullable=false) | ||
* | ||
* @var self | ||
*/ | ||
public $parent; | ||
|
||
public function __construct() | ||
{ | ||
$this->id = uniqid(); | ||
} | ||
} | ||
|
||
/** | ||
* @ORM\Entity | ||
*/ | ||
class GH7877EntityWithNullableAssociation | ||
{ | ||
/** | ||
* @ORM\Id | ||
* @ORM\Column(type="string") | ||
* @ORM\GeneratedValue(strategy="NONE") | ||
* | ||
* @var string | ||
*/ | ||
public $id; | ||
|
||
/** | ||
* @ORM\ManyToOne(targetEntity="GH7877EntityWithNullableAssociation") | ||
* @ORM\JoinColumn(name="parent_id", referencedColumnName="id", nullable=true) | ||
* | ||
* @var self | ||
*/ | ||
public $parent; | ||
|
||
public function __construct() | ||
{ | ||
$this->id = uniqid(); | ||
} | ||
} |