BonBonSlick
@BonBonSlick
Junior Web Developer Trainee

Почему уникальный индекс выдает ошибку при каскадной операции?

маппинг емейла
<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>

маппинг юзера
<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>


Каждый юзер может иметь только 1 активный главный или запасной емейл. Но при попытке обновить даже один единственный емейл выдает ошибку уникальности.
SQL нативный проходит, скорее всего где-то доктрина тупит и сбивает с толку.
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';

Пример запроса который спокойно пройдет, если выполнять напрямую.

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->emails[$keyEmail] = $email;
Даже если емейлы имеют ункиальные isActive и isFallback, то все равно ошибка.
Словно дважды выполнен запрос.
Делать персист раздельно емейла и юзера пробовал, юез каскадного.
Там выдает немного иные ошибки
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", "admin-new@mail.com", 
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 [] []

или

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\UserEmail@00000000074fae50000000005adffd16. 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.

смотря на ком каскад персист и кого персистим.
  • Вопрос задан
  • 24 просмотра
Решения вопроса 1
BonBonSlick
@BonBonSlick Автор вопроса
Junior Web Developer Trainee
Все дело в том, что при добавлении нового емейла идет фильтр и отключение старых
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);
                }
            }
        );
    }

Из-за каскадной операции cascade-persist на сущности User
<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>
 ...


Ожидаемое поведение, что в начале произойдет обновление отключеных записей, но увы.
Доктрина делает вставку новых сущностей перед обновлением старых.
То есть в каскадных операциях, операции вставки приоритетнее обновлений.
Хотелось бы знать, можно ли как-то изменить приоритет для конкретных связей, что вряд ли.
Решение - мануально обновить старые сущности.
То есть
$entity->disableOldEmails(); // ранее метод был приватным
            foreach ($entity->emails() as $email) {
                $this->userEmailRepository->save($email); // мануально персистим и сразу делаем флаш, коммит
            }

только после етого можно добавлять новые сущности в коллекцию.

Такое себе решение, ведь будет + N количество вставок в БД.
Пока лучше не придумал.
Ответ написан
Пригласить эксперта
Ваш ответ на вопрос

Войдите, чтобы написать ответ

Войти через центр авторизации
Похожие вопросы