DDC-3354: Replacing indexed item on association with indexBy cannot comply with unicity constraint #4147

Open
opened 2026-01-22 14:36:07 +01:00 by admin · 1 comment
Owner

Originally created by @doctrinebot on GitHub (Oct 17, 2014).

Originally assigned to: @beberlei on GitHub.

Jira issue originally created by user cblegare:

Using a bidirectional one-to-many relation having an 'indexBy' clause on the owning side and a unique constraint over the indexed field and the 'mappedBy' field of the inverse side, replaced indexed fields being inserted before removed violated the unique constraint.

As stated in the code examples below, if done without the unique constraint, when replacing an indexed field, the row is replaced in the database (new one created, old one deleted).

Here is an example:

Person:
    type: entity
    id:
        id:
            type: integer
            generator:
                strategy: AUTO
    oneToMany:
        emailAddresses:
            targetEntity: Email
            indexBy: name
            mappedBy: owner
            cascade: [ all ]
            orphanRemoval: true

Email:
    type: entity
    id:
        id:
            type: integer
            generator:
                strategy: AUTO
    fields:
        name:
            type: string
            nullable: false
        address:
            type: string
            nullable: false
    uniqueConstraints:
        unique*named_person*email:
            columns: [ owner_id, name ]
    manyToOne:
        owner:
            targetEntity: Person
            inversedBy:   emailAddresses
            joinColumn:
                name: owner_id
                referencedColumnName: id
class Person {
    protected $id;
    /****
     * @var Collection|Email[] $emailAddresses
     */
    protected $emailAddresses;

    /****
     * I would expect this to work.
     */
    public function setEmailAddress_expected($name, $emailAddress)
    {
        /****
         * Expected to work but throws UniqueConstraintViolationException
         * If done without the 'unique*named_person*email' constraint', it 
         * works properly creating a new Email and deleting the old one.
         */
        $this->emailAddresses->set(
            (string) $name,
            new Email($this, (string) $name, (string) $emailAddress)
        );
    }

    /****
     * I would expect this to work.
     */
    public function setEmailAddress*expected*too($name, $emailAddress)
    {
        /****
         * Expected to work but throws UniqueConstraintViolationException
         * If done without the 'unique*named_person*email' constraint, it 
         * works properly creating a new Email and deleting the old one.
         */
        $this->emailAddresses->remove((string) $name);
        $this->emailAddresses->set(
            (string) $name,
            new Email($this, (string) $name, (string) $emailAddress)
        );
    }

    /****
     * Works
     */
    public function setEmailAddress_works($name, $emailAddress)
    {
        $existing = $this->emailAddresses->get((string) $name);
        if ($existing) {
            $existing->setAddress((string) $emailAddress);
        } else {
            $this->emailAddresses->set(
                (string) $name,
                new Email($this, (string) $name, (string) $emailAddress)
            );
        }
    }
}

class Email {
    protected $id;
    protected $owner;
    protected $name;
    protected $address;
    public function **construct($owner, $name, $address)
    {
        $this->owner = $owner;
        $this->name = $name;
        $this->address = $address;
    }

    /****
     * I am forced to create this method.
     */
    public function setAddress($address)
    {
        $this->address = $address;
    }
}

IMO, Using 'indexBy' on a one-to-many relation should automatically generate a unique constraint for the combination of the indexed field and the 'mappedBy' field.

Documentation states that
{quote}
Fields that are used for the index by feature HAVE to be unique in the database. The behavior for multiple entities with the same index-by field value is undefined.
{quote}
which is unclear (especially the use of the word 'database'). I wonder why indexes used for association should be unique for a whole table.

Originally created by @doctrinebot on GitHub (Oct 17, 2014). Originally assigned to: @beberlei on GitHub. Jira issue originally created by user cblegare: Using a bidirectional one-to-many relation having an 'indexBy' clause on the owning side and a unique constraint over the indexed field and the 'mappedBy' field of the inverse side, replaced indexed fields being inserted before removed violated the unique constraint. As stated in the code examples below, if done without the unique constraint, when replacing an indexed field, the row is replaced in the database (new one created, old one deleted). Here is an example: ``` Person: type: entity id: id: type: integer generator: strategy: AUTO oneToMany: emailAddresses: targetEntity: Email indexBy: name mappedBy: owner cascade: [ all ] orphanRemoval: true Email: type: entity id: id: type: integer generator: strategy: AUTO fields: name: type: string nullable: false address: type: string nullable: false uniqueConstraints: unique*named_person*email: columns: [ owner_id, name ] manyToOne: owner: targetEntity: Person inversedBy: emailAddresses joinColumn: name: owner_id referencedColumnName: id ``` ``` class Person { protected $id; /**** * @var Collection|Email[] $emailAddresses */ protected $emailAddresses; /**** * I would expect this to work. */ public function setEmailAddress_expected($name, $emailAddress) { /**** * Expected to work but throws UniqueConstraintViolationException * If done without the 'unique*named_person*email' constraint', it * works properly creating a new Email and deleting the old one. */ $this->emailAddresses->set( (string) $name, new Email($this, (string) $name, (string) $emailAddress) ); } /**** * I would expect this to work. */ public function setEmailAddress*expected*too($name, $emailAddress) { /**** * Expected to work but throws UniqueConstraintViolationException * If done without the 'unique*named_person*email' constraint, it * works properly creating a new Email and deleting the old one. */ $this->emailAddresses->remove((string) $name); $this->emailAddresses->set( (string) $name, new Email($this, (string) $name, (string) $emailAddress) ); } /**** * Works */ public function setEmailAddress_works($name, $emailAddress) { $existing = $this->emailAddresses->get((string) $name); if ($existing) { $existing->setAddress((string) $emailAddress); } else { $this->emailAddresses->set( (string) $name, new Email($this, (string) $name, (string) $emailAddress) ); } } } class Email { protected $id; protected $owner; protected $name; protected $address; public function **construct($owner, $name, $address) { $this->owner = $owner; $this->name = $name; $this->address = $address; } /**** * I am forced to create this method. */ public function setAddress($address) { $this->address = $address; } } ``` IMO, Using 'indexBy' on a one-to-many relation should automatically generate a unique constraint for the combination of the indexed field and the 'mappedBy' field. [Documentation](http://doctrine-orm.readthedocs.org/en/latest/tutorials/working-with-indexed-associations.html) states that {quote} Fields that are used for the index by feature HAVE to be unique in the database. The behavior for multiple entities with the same index-by field value is undefined. {quote} which is unclear (especially the use of the word 'database'). I wonder why indexes used for association should be unique for a whole table.
admin added the Bug label 2026-01-22 14:36:07 +01:00
Author
Owner

@doctrinebot commented on GitHub (Oct 19, 2014):

Comment created by @ocramius:

{quote}I wonder why indexes used for association should be unique for a whole table.{quote}

That's because we can't ensure that indexes aren't overwritten when hydrating a collection: that is what the behavior "undefined" stands for.

As for replacing values in the collection, that's how the ORM works in any case, as it inserts data before removing any data to avoid removes from causing FK constraint failures (see d361ed904e/lib/Doctrine/ORM/UnitOfWork.php (L347-L379))

@doctrinebot commented on GitHub (Oct 19, 2014): Comment created by @ocramius: {quote}I wonder why indexes used for association should be unique for a whole table.{quote} That's because we can't ensure that indexes aren't overwritten when hydrating a collection: that is what the behavior "undefined" stands for. As for replacing values in the collection, that's how the ORM works in any case, as it inserts data before removing any data to avoid removes from causing FK constraint failures (see https://github.com/doctrine/doctrine2/blob/d361ed904e5d56711304b755b5b2a8484d9a35b6/lib/Doctrine/ORM/UnitOfWork.php#L347-L379)
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: doctrine/archived-orm#4147