Skip to content

EntityIdentityCollisionException in OneToOne Id column with inheritance #12428

Description

@janatjak

Bug Report

Q A
Version 3.*

Summary

Using entity (with inheritance) as ID (by OneToOne) throw EntityIdentityCollisionException

BusinessCase:
    id: int
    contract: Contract (bidirectional OneToOne)

Contract:
    id: BusinessCase (bidirectional OneToOne)

Attachment:
    id: int
    contract: Contract
$entityManager->find(Attachment::class, 1); // throw Exception

Current behavior

Doctrine\ORM\Exception\EntityIdentityCollisionException: While adding an entity of class Doctrine\Tests\Proxies\__CG__\Doctrine\Tests\ORM\Functional\Ticket\GH12428Contract with an ID hash of "1" to the identity map,
another object of class Doctrine\Tests\ORM\Functional\Ticket\GH12428Contract was already present for the same ID. This exception
is a safeguard against an internal inconsistency - IDs should uniquely map to
entity object instances. This problem may occur if:

- you use application-provided IDs and reuse ID values;
- database-provided IDs are reassigned after truncating the database without
clearing the EntityManager;
- you might have been using EntityManager#getReference() to create a reference
for a nonexistent ID that was subsequently (by the RDBMS) assigned to another
entity.

Expected behavior

Don't allow add entity to identity map twice.

How to reproduce

<?php

declare(strict_types=1);

namespace Doctrine\Tests\ORM\Functional\Ticket;

use Doctrine\ORM\Mapping as ORM;
use Doctrine\Tests\OrmFunctionalTestCase;
use PHPUnit\Framework\Attributes\Group;

/** @see https://github.com/doctrine/orm/issues/12428 */
#[Group('GH12428')]
class GH12428Test extends OrmFunctionalTestCase
{
    protected function setUp(): void
    {
        parent::setUp();

        $this->createSchemaForModels(
            GH12428BusinessCase::class,
            GH12428Contract::class,
            GH12428Attachment::class,
        );
    }

    public function testIssue(): void
    {
        $businessCase = new GH12428ABusinessCase();
        $attachment   = new GH12428Attachment($businessCase->contract);

        $this->_em->persist($businessCase);
        $this->_em->persist($attachment);
        $this->_em->flush();
        $this->_em->clear();

        $loadedAttachment = $this->_em->find(GH12428Attachment::class, $attachment->id);
        self::assertNotNull($loadedAttachment);
    }
}

#[ORM\Entity]
#[ORM\InheritanceType('SINGLE_TABLE')]
#[ORM\DiscriminatorMap([
    'A' => GH12428ABusinessCase::class,
])]
abstract class GH12428BusinessCase
{
    #[ORM\Id]
    #[ORM\Column]
    #[ORM\GeneratedValue]
    public int|null $id = null;

    #[ORM\OneToOne(mappedBy: 'id', cascade: ['persist'])]
    public GH12428Contract $contract;

    public function __construct()
    {
        $this->contract = new GH12428Contract($this);
    }
}

#[ORM\Entity]
class GH12428ABusinessCase extends GH12428BusinessCase
{
}

#[ORM\Entity]
class GH12428Attachment
{
    #[ORM\Id]
    #[ORM\Column]
    #[ORM\GeneratedValue]
    public int|null $id = null;

    public function __construct(
        #[ORM\ManyToOne]
        public GH12428Contract $contract,
    ) {
    }
}

#[ORM\Entity]
class GH12428Contract
{
    public function __construct(
        #[ORM\Id]
        #[ORM\OneToOne(inversedBy: 'contract')]
        #[ORM\JoinColumn(name: 'id')]
        public GH12428BusinessCase $id,
    ) {
    }
}

Test case: janatjak@0dae9be
Result: https://github.com/janatjak/orm/actions/runs/24121411601/job/70376040902

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions