Skip to content

Commit a616914

Browse files
authored
Turn identity map collisions from exception to deprecation notice (#10878)
In #10785, a check was added that prevents entity instances from getting into the identity map when another object for the same ID is already being tracked. This caused regressions for users that work with application-provided IDs and expect this condition to fail with `UniqueConstraintViolationExceptions` when flushing to the database. Thus, this PR turns the exception into a deprecation notice. Users can opt-in to the new behavior. In 3.0, the exception will be used. Implements #10871.
1 parent fd0bdc6 commit a616914

File tree

5 files changed

+86
-7
lines changed

5 files changed

+86
-7
lines changed

UPGRADE.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,19 @@
11
# Upgrade to 2.16
22

3+
## Deprecated accepting duplicate IDs in the identity map
4+
5+
For any given entity class and ID value, there should be only one object instance
6+
representing the entity.
7+
8+
In https://github.com/doctrine/orm/pull/10785, a check was added that will guard this
9+
in the identity map. The most probable cause for violations of this rule are collisions
10+
of application-provided IDs.
11+
12+
In ORM 2.16.0, the check was added by throwing an exception. In ORM 2.16.1, this will be
13+
changed to a deprecation notice. ORM 3.0 will make it an exception again. Use
14+
`\Doctrine\ORM\Configuration::setRejectIdCollisionInIdentityMap()` if you want to opt-in
15+
to the new mode.
16+
317
## Potential changes to the order in which `INSERT`s are executed
418

519
In https://github.com/doctrine/orm/pull/10547, the commit order computation was improved

lib/Doctrine/ORM/Configuration.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1117,4 +1117,14 @@ public function setLazyGhostObjectEnabled(bool $flag): void
11171117

11181118
$this->_attributes['isLazyGhostObjectEnabled'] = $flag;
11191119
}
1120+
1121+
public function setRejectIdCollisionInIdentityMap(bool $flag): void
1122+
{
1123+
$this->_attributes['rejectIdCollisionInIdentityMap'] = $flag;
1124+
}
1125+
1126+
public function isRejectIdCollisionInIdentityMapEnabled(): bool
1127+
{
1128+
return $this->_attributes['rejectIdCollisionInIdentityMap'] ?? false;
1129+
}
11201130
}

lib/Doctrine/ORM/UnitOfWork.php

Lines changed: 40 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1635,8 +1635,10 @@ public function addToIdentityMap($entity)
16351635

16361636
if (isset($this->identityMap[$className][$idHash])) {
16371637
if ($this->identityMap[$className][$idHash] !== $entity) {
1638-
throw new RuntimeException(sprintf(
1639-
<<<'EXCEPTION'
1638+
if ($this->em->getConfiguration()->isRejectIdCollisionInIdentityMapEnabled()) {
1639+
throw new RuntimeException(
1640+
sprintf(
1641+
<<<'EXCEPTION'
16401642
While adding an entity of class %s with an ID hash of "%s" to the identity map,
16411643
another object of class %s was already present for the same ID. This exception
16421644
is a safeguard against an internal inconsistency - IDs should uniquely map to
@@ -1651,11 +1653,42 @@ public function addToIdentityMap($entity)
16511653
16521654
Otherwise, it might be an ORM-internal inconsistency, please report it.
16531655
EXCEPTION
1654-
,
1655-
get_class($entity),
1656-
$idHash,
1657-
get_class($this->identityMap[$className][$idHash])
1658-
));
1656+
,
1657+
get_class($entity),
1658+
$idHash,
1659+
get_class($this->identityMap[$className][$idHash])
1660+
)
1661+
);
1662+
} else {
1663+
Deprecation::trigger(
1664+
'doctrine/orm',
1665+
'https://github.com/doctrine/orm/pull/10785',
1666+
<<<'EXCEPTION'
1667+
While adding an entity of class %s with an ID hash of "%s" to the identity map,
1668+
another object of class %s was already present for the same ID. This will trigger
1669+
an exception in ORM 3.0.
1670+
1671+
IDs should uniquely map to entity object instances. This problem may occur if:
1672+
1673+
- you use application-provided IDs and reuse ID values;
1674+
- database-provided IDs are reassigned after truncating the database without
1675+
clearing the EntityManager;
1676+
- you might have been using EntityManager#getReference() to create a reference
1677+
for a nonexistent ID that was subsequently (by the RDBMS) assigned to another
1678+
entity.
1679+
1680+
Otherwise, it might be an ORM-internal inconsistency, please report it.
1681+
1682+
To opt-in to the new exception, call
1683+
\Doctrine\ORM\Configuration::setRejectIdCollisionInIdentityMap on the entity
1684+
manager's configuration.
1685+
EXCEPTION
1686+
,
1687+
get_class($entity),
1688+
$idHash,
1689+
get_class($this->identityMap[$className][$idHash])
1690+
);
1691+
}
16591692
}
16601693

16611694
return false;

tests/Doctrine/Tests/ORM/Functional/BasicFunctionalTest.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1329,6 +1329,8 @@ public function testWrongAssociationInstance(): void
13291329

13301330
public function testItThrowsWhenReferenceUsesIdAssignedByDatabase(): void
13311331
{
1332+
$this->_em->getConfiguration()->setRejectIdCollisionInIdentityMap(true);
1333+
13321334
$user = new CmsUser();
13331335
$user->name = 'test';
13341336
$user->username = 'test';

tests/Doctrine/Tests/ORM/UnitOfWorkTest.php

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -927,8 +927,28 @@ public function testRemovedEntityIsRemovedFromOneToManyCollection(): void
927927
self::assertEmpty($user->phonenumbers->getSnapshot());
928928
}
929929

930+
public function testItTriggersADeprecationNoticeWhenApplicationProvidedIdsCollide(): void
931+
{
932+
// We're using application-provided IDs and assign the same ID twice
933+
// Note this is about colliding IDs in the identity map in memory.
934+
// Duplicate database-level IDs would be spotted when the EM is flushed.
935+
936+
$phone1 = new CmsPhonenumber();
937+
$phone1->phonenumber = '1234';
938+
$this->_unitOfWork->persist($phone1);
939+
940+
$phone2 = new CmsPhonenumber();
941+
$phone2->phonenumber = '1234';
942+
943+
$this->expectDeprecationWithIdentifier('https://github.com/doctrine/orm/pull/10785');
944+
945+
$this->_unitOfWork->persist($phone2);
946+
}
947+
930948
public function testItThrowsWhenApplicationProvidedIdsCollide(): void
931949
{
950+
$this->_emMock->getConfiguration()->setRejectIdCollisionInIdentityMap(true);
951+
932952
// We're using application-provided IDs and assign the same ID twice
933953
// Note this is about colliding IDs in the identity map in memory.
934954
// Duplicate database-level IDs would be spotted when the EM is flushed.

0 commit comments

Comments
 (0)