Bug: UnitOfWork refresh attempts to load the wrong object #7333

Open
opened 2026-01-22 15:50:04 +01:00 by admin · 0 comments
Owner

Originally created by @Novynn on GitHub (Mar 2, 2024).

Bug Report

Q A
BC Break no
Version 2.18.0

Summary

When calling refresh on an entity, under certain circumstances the id used to fetch the entity from the database can be wrong.

Current behavior

Confusing the UnitOfWork by creating a new entity with the same id as one that it is already being managed can cause OID collisions when that entity is garbage collected due to not having a reference in the UnitOfWork's identityMap. This leads to a refresh attempting to use the wrong id in the database.

How to reproduce

#[Entity]
#[Table("my_entity")]
class MyEntity
{
    #[Id, Column(name: "id", type: "integer"), GeneratedValue(strategy: 'SEQUENCE')]
    public ?int $id = null;
}

#[Entity]
#[Table("my_token")]
class MyToken
{
    public function __construct(
        #[Id, Column(name: "hash", type: "string")]
        public string $hash,
    ) {}
}

// NOTE: Seed the database with a single my_token
$key = md5(random_bytes(4));

$em->getConnection()->executeStatement(<<<SQL
    DELETE FROM my_token;
    INSERT INTO my_token VALUES ('{$key}');
SQL);

function refreshMyToken(EntityManager $em): void
{
    global $key;

    $token = $em->find(MyToken::class, $key);
    echo "MyToken (original) has oid of ".spl_object_id($token)."\n";

    // NOTE: This causes all the problems, as the UnitOfWork still thinks the token is active and managed
    $em->createQuery('DELETE FROM MyToken s')->execute();

    $token = new MyToken($key);
    // NOTE: This causes the deprecation warning in addToIdentityMap, but continues
    $em->persist($token);
    $em->flush();

    echo "MyToken (new) has oid of ".spl_object_id($token)."\n";

    gc_collect_cycles();
}

refreshMyToken($em);

$entity = new MyEntity();
$em->persist($entity);
$em->flush();
echo "Entity has oid of ".spl_object_id($entity)."\n";

\Closure::fromCallable(
    /** @psalm-suppress InaccessibleProperty */
    function() {
        /** @var \Doctrine\ORM\UnitOfWork $this */
        echo "UnitOfWork identifier map is: ".var_export($this->entityIdentifiers, true)."\n";
    },
)->call($em->getUnitOfWork());

$em->refresh($entity);

Output:

MyToken (original) has oid of 769
MyToken (new) has oid of 765
Entity has oid of 765
UnitOfWork identifier map is: array (
  769 =>
  array (
    'hash' => 'b8ff315c79068f22ccf4543cd93f9f46',
  ),
  765 =>
  array (
    'hash' => 'b8ff315c79068f22ccf4543cd93f9f46',
  ),
)

Fatal error: Uncaught PDOException: SQLSTATE[22P02]: Invalid text representation: 7 ERROR:  invalid input syntax for type integer: "b8ff315c79068f22ccf4543cd93f9f46"

Expected behavior

In the future the exception in #10785 will be thrown, but currently the code continues as normal in a broken state until the mismatched column types stop execution.

Originally created by @Novynn on GitHub (Mar 2, 2024). ### Bug Report | Q | A |------------ | ------ | BC Break | no | Version | 2.18.0 #### Summary When calling refresh on an entity, under certain circumstances the id used to fetch the entity from the database can be wrong. #### Current behavior Confusing the `UnitOfWork` by creating a new entity with the same id as one that it is already being managed can cause OID collisions when that entity is garbage collected due to not having a reference in the `UnitOfWork`'s `identityMap`. This leads to a `refresh` attempting to use the wrong id in the database. #### How to reproduce ```php #[Entity] #[Table("my_entity")] class MyEntity { #[Id, Column(name: "id", type: "integer"), GeneratedValue(strategy: 'SEQUENCE')] public ?int $id = null; } #[Entity] #[Table("my_token")] class MyToken { public function __construct( #[Id, Column(name: "hash", type: "string")] public string $hash, ) {} } // NOTE: Seed the database with a single my_token $key = md5(random_bytes(4)); $em->getConnection()->executeStatement(<<<SQL DELETE FROM my_token; INSERT INTO my_token VALUES ('{$key}'); SQL); function refreshMyToken(EntityManager $em): void { global $key; $token = $em->find(MyToken::class, $key); echo "MyToken (original) has oid of ".spl_object_id($token)."\n"; // NOTE: This causes all the problems, as the UnitOfWork still thinks the token is active and managed $em->createQuery('DELETE FROM MyToken s')->execute(); $token = new MyToken($key); // NOTE: This causes the deprecation warning in addToIdentityMap, but continues $em->persist($token); $em->flush(); echo "MyToken (new) has oid of ".spl_object_id($token)."\n"; gc_collect_cycles(); } refreshMyToken($em); $entity = new MyEntity(); $em->persist($entity); $em->flush(); echo "Entity has oid of ".spl_object_id($entity)."\n"; \Closure::fromCallable( /** @psalm-suppress InaccessibleProperty */ function() { /** @var \Doctrine\ORM\UnitOfWork $this */ echo "UnitOfWork identifier map is: ".var_export($this->entityIdentifiers, true)."\n"; }, )->call($em->getUnitOfWork()); $em->refresh($entity); ``` Output: ``` MyToken (original) has oid of 769 MyToken (new) has oid of 765 Entity has oid of 765 UnitOfWork identifier map is: array ( 769 => array ( 'hash' => 'b8ff315c79068f22ccf4543cd93f9f46', ), 765 => array ( 'hash' => 'b8ff315c79068f22ccf4543cd93f9f46', ), ) Fatal error: Uncaught PDOException: SQLSTATE[22P02]: Invalid text representation: 7 ERROR: invalid input syntax for type integer: "b8ff315c79068f22ccf4543cd93f9f46" ``` #### Expected behavior In the future the exception in #10785 will be thrown, but currently the code continues as normal in a broken state until the mismatched column types stop execution.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: doctrine/archived-orm#7333