Доктрина транзакционных границ

При вызове этого кода он вызовет событие postPersist ORM Doctrine. У меня есть прослушиватель событий для этого события, который будет отправлять события агрегата в локальную (синхронную) шину событий и сохранять события, помеченные как события интеграции, в таблице исходящих сообщений.

Все это должно произойти в рамках одной транзакции. Мой вопрос: будет ли этот код работать так, как я ожидаю? Фактический агрегат должен сохраняться, а события в исходящих сообщениях должны сохраняться в рамках одной транзакции.

public function persist(Aggregate $aggregate): void
{
    $this->getEntityManager()->getConnection()->beginTransaction();

    try {
        $this->getEntityManager()->persist($aggregate);
        $this->getEntityManager()->flush();
        $this->getEntityManager()->getConnection()->commit();
    } catch (Exception $e) {
        $this->getEntityManager()->getConnection()->rollBack();
        throw $e;
    }
}

На диаграмме ниже показана вся архитектура. Состояние текущей транзакции в приложении должно быть гарантировано до того, как будут созданы события интеграции.

Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Symfony Station Communiqué - 7 июля 2023 г
Symfony Station Communiqué - 7 июля 2023 г
Это коммюнике первоначально появилось на Symfony Station .
Symfony Station Communiqué - 17 февраля 2023 г
Symfony Station Communiqué - 17 февраля 2023 г
Это коммюнике первоначально появилось на Symfony Station , вашем источнике передовых новостей Symfony, PHP и кибербезопасности.
Управление ответами api для исключений на Symfony с помощью KernelEvents
Управление ответами api для исключений на Symfony с помощью KernelEvents
Много раз при создании api нам нужно возвращать клиентам разные ответы в зависимости от возникшего исключения.
1
0
60
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Событие Doctrine postPersist будет отправлено при вызове метода flush(). Если вы выполняете еще одно сохранение/сброс для сохранения событий в таблице исходящих сообщений (часть кода, которую вы нам не показали, так что я предполагаю, что здесь), то вы запустите вложенную транзакцию. В этом случае всю операцию можно рассматривать как одну транзакцию, если между ними не создается точка сохранения.


Однако вам следует быть осторожным с транзитивными операциями, выполняемыми во время прослушивания postPersist, поскольку ваша исходная транзакция в этот момент все еще открыта и заключена в try/catch. В случае возникновения какой-либо ошибки будет откачена вся транзакция (включая родительскую в случае вложенных транзакций).

Я сказал это, потому что вижу, что вы используете шину событий домена, настроенную под транспорт sync в памяти. При этом подписчики событий вашего домена будут немедленно выполняться в том же потоке PHP, и они также будут включены в одну и ту же транзакцию. На мой взгляд, это проблема проектирования, и вы можете столкнуться с неожиданными откатами из-за сбоя любого подписчика.

Чтобы избежать этого, я бы предложил изменить конфигурацию шины событий, чтобы вместо этого использовать асинхронный транспорт, отделив вашу совокупную операцию сохранения от любого подписчика на события домена. Если вы действительно хотите это сделать, то, вероятно, бесполезно реализовывать шаблон шины в этом сценарии, компонента Symfony EventDispatcher может быть достаточно.


В качестве примечания по оптимизации: текущая функция persist(Aggregate $aggregate) не нуждается в попытке/перехвате, если вы не включаете какие-либо другие операции сохранения между ними. Метод flush() уже создает для вас транзакцию и попытку/вылов. Таким образом, любой сбой, происходящий в ваших слушателях Doctrine, будет обнаружен, и транзакция будет отменена.

Он ведет себя именно так:

public function persist(Aggregate $aggregate): void
{
    $this->getEntityManager()->persist($aggregate);
    $this->getEntityManager()->flush();
}

Спасибо за очень хороший ответ. На мой взгляд, проблема дизайна — это решение, а не проблема. События домена отправляются сразу, и прослушиватели в том же контексте, который вызывает исключение, должны откатить всю транзакцию, включая вставки в исходящие сообщения. Если состояние агрегата И исходящего почтового ящика сохраняется, то и только тогда внешние стороны будут проинформированы через асинхронную шину RMQ. Может быть, я что-то упускаю?

floriank 09.04.2024 20:23

Ничего страшного, если это соответствует правилам вашего проекта. В моем случае мы не откатываем основную операцию, если инструкция по отправке электронной почты завершается сбоем у подписчика событий домена (например, из-за сбоя сервера). Оно будет отправлено повторно позже. Согласно DDD, подход к событиям предметной области должен представлять собой отдельный механизм для интеграции с внешней системой или выполнения асинхронной обработки.

yceruto 09.04.2024 21:14

Упомянутый вами отделенный механизм называется «событиями интеграции», то есть событиями, которые относятся к внешним системам и распространяются на внешние службы. Для событий домена, которые будут использоваться другими элементами в локальной системе, вполне подойдет размещение в памяти. Идея состоит в том, чтобы отправлять внешние события только после того, как локальные транзакции и их побочные эффекты были устранены, чтобы обеспечить согласованность. Я ценю обсуждение! :) Если у вас другое мнение, поделитесь им.

floriank 09.04.2024 22:59

Другие вопросы по теме