B
B
BonBon Slick2021-05-30 13:54:59
Doctrine ORM
BonBon Slick, 2021-05-30 13:54:59

Why does a unique index throw an error on a cascading operation?

email mapping

<unique-constraints>
             <unique-constraint columns="user_uuid,is_active,is_fallback" name="unique_user_active_email"/>
        </unique-constraints>
...


        <many-to-one target-entity="App\Domain\UserPack\User\Entity\User"
                     field="user"
                     inversed-by="emails"
        >
            <cascade>
                <cascade-persist/>
            </cascade>
            <join-column name="user_uuid" referenced-column-name="uuid"/>
        </many-to-one>

user mapping
<one-to-many target-entity="App\Domain\UserPack\Email\Entity\UserEmail"
                     mapped-by="user"
                     field="emails"
                     index-by="email.email"
                     orphan-removal="true"
        >
            <cascade>
                <cascade-persist/>
                <cascade-remove/>
            </cascade>
            <order-by>
                <order-by-field name="createdAt" direction="DESC"/>
            </order-by>
        </one-to-many>


Each user can only have 1 active primary or secondary email. But when you try to update even one single email, it gives a uniqueness error.
SQL native passes, most likely somewhere the doctrine blunts and confuses.
UPDATE user_email SET  updated_at = '2021-05-30 10:30:41', is_active = false, is_fallback = false WHERE uuid = 'c2c54260-c130-11eb-8a5b-004e01bcde28';

An example of a query that will pass quietly if executed directly.

EXCEPTION /.../vendor/doctrine/dbal/lib/Doctrine/DBAL/Driver/AbstractSQLiteDriver.php. 
Message: An exception occurred while executing
'UPDATE user_email SET updated_at = ?, is_active = ? WHERE uuid = ?'
with params ["2021-05-30 10:48:01", 0, "7ebf86ee-c134-11eb-911b-004e01bcde28"]:
SQLSTATE[23000]: Integrity constraint violation: 19
UNIQUE constraint failed: user_email.user_uuid, user_email.is_active. Code: 0 [] []

This error is thrown when a new email is added to the indexed collection.
$this->emails[$keyEmail] = $email;
Even if emails have unique isActive and isFallback, it's still an error.
As if the request was executed twice.
I tried to make the persistence separately for the email and the user, using the cascade.
It gives slightly different errors.
EXCEPTION /...vendor/symfony/doctrine-bridge/Messenger/DoctrineTransactionMiddleware.php. 
Message: An exception occurred while executing 
'NSERT INTO user_email (created_at, updated_at, uuid, email, is_fallback, is_active, user_uuid) VALUES (?, ?, ?, ?, ?, ?, ?)' with 
params ["2021-05-30 11:04:58", "2021-05-30 11:04:58", "df5ada06-c136-11eb-b113-004e01bcde28", "[email protected]", 
0, 1, "de1204bc-c136-11eb-95d7-004e01bcde28"]:  
SQLSTATE[23000]: Integrity constraint violation: 19 UNIQUE constraint failed: user_email.user_uuid, user_email.is_active, user_email.is_fallback. Code: 0 [] []

or

A new entity was found through the relationship 'App\Domain\UserPack\User\Entity\User#emails' that was not 
configured to cascade persist operations for entity: 
App\Domain\UserPack\Email\Entity\[email protected] To solve this issue: Either explicitly 
call EntityManager#persist() on this unknown entity or configure cascade persist this association in the mapping for example 
@ManyToOne(..,cascade={"persist"}). If you cannot find out which entity causes the problem implement 
'App\Domain\UserPack\Email\Entity\UserEmail#__toString()' to get a clue.

depending on whom the cascade persists and whom we persist.

Answer the question

In order to leave comments, you need to log in

1 answer(s)
B
BonBon Slick, 2021-05-30
@BonBonSlick

The thing is that when adding a new email, there is a filter and turning off the old ones.

public function disableOldEmails(bool $isFallbackEmailToDisable = false): void {
        $updateDto = new UserEmailUpdateDTO(false);
        $this->emails->map(
            static function (UserEmail $relatedEmail) use ($isFallbackEmailToDisable, $updateDto) {
                if ($isFallbackEmailToDisable === $relatedEmail->isFallback()->value()) {
                    $relatedEmail->update($updateDto);
                }
            }
        );
    }

Due to the cascade-persist operation on the User entity
<one-to-many target-entity="App\Domain\UserPack\Email\Entity\UserEmail"
                     mapped-by="user"
                     field="emails"
                     index-by="email.email"
                     orphan-removal="true"
        >
            <cascade>
                <cascade-persist/> ////// <---
                <cascade-remove/>
            </cascade>
 ...

The expected behavior is that disabled entries will be updated first, but alas.
Doctrine does the insertion of new entities before updating old ones.
That is, in cascading operations, inserts take precedence over updates.
I would like to know if it is possible to somehow change the priority for specific links, which is unlikely.
The solution is to manually update the old entities.
I.e
$entity->disableOldEmails(); // ранее метод был приватным
            foreach ($entity->emails() as $email) {
                $this->userEmailRepository->save($email); // мануально персистим и сразу делаем флаш, коммит
            }

only after that you can add new entities to the collection.
Such a solution, because there will be + N the number of inserts in the database.
Until I came up with a better idea.

Didn't find what you were looking for?

Ask your question

Ask a Question

731 491 924 answers to any question