DDD: отношение «многие ко многим» между двумя различными агрегатами - PullRequest
0 голосов
/ 20 февраля 2020

У меня есть два "больших" объекта или агрегата, которые имеют свои собственные бизнес-логики c - они сохраняются, обновляются и уничтожаются в отдельных транзакциях. У них есть собственные дочерние объекты, которыми манипулируют через эти совокупные корни. Но проблема в том, что эти два агрегата должны быть связаны между собой многими. С точки зрения пользовательского интерфейса, существует своего рода пользовательский интерфейс, в котором один уже существующий экземпляр второго агрегата добавляется в первый агрегат. С точки зрения базы данных, существует таблица, которая содержит внешние ключи для таблиц первого и второго агрегатов

entity_one_id | entity_two_id
1             | 2
1             | 3
1             | 4

В приведенном выше примере экземпляр первого агрегата содержит ссылки на второй агрегат.

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

aggregateOne = aggregateOneRepository->getById(1);
....
aggregate2 = aggregateTwoRepository->getById(2);
aggregate3 = aggregateTwoRepository->getById(3);
aggregate4 = aggregateTwoRepository->getById(4);
aggregateOne->addChildAggregate(aggregate2);
aggregateOne->addChildAggregate(aggregate3);
aggregateOne->addChildAggregate(aggregate4);
aggregateOneRepository->update(aggregateOne);

Кажется, что в этой транзакции я не изменяю второй агрегат и меняю только один агрегат. Но я не уверен, что теория DDD позволяет загружать несколько разных агрегатов при сохранении одного агрегата. Итак, этот код нарушает теорию или нет?

Ответы [ 3 ]

2 голосов
/ 21 февраля 2020

Совокупность root не должна содержать экземпляров других агрегатных корней. Агрегату может быть передана переходная ссылка на другую, когда, например, вызывается метод, но он не сохраняется на этой ссылке. Он используется только в вызове.

Ваш пример на самом деле более распространен, чем вы можете себе представить. Если бы нам пришлось перейти на совокупности Order и Product, то мы получили бы отношение многие ко многим. OrderItem представляет это отношение и лучше всего определяется как объект значения.

Когда вы обнаружите, что вам нужно «ссылаться» на другой агрегат, тогда лучше использовать либо только идентификатор, либо некоторый объект значения, который содержит как минимум идентификатор другого агрегата.

У меня немного другое представление о транзакциях. Агрегат root является границей согласованности и, как таковой, вполне вписывается в границу транзакции. Необходимо предпринять все попытки сохранить единый агрегат внутри транзакции, но вам также нужно быть прагматичным c по этому поводу. Если вам нужен высокий уровень согласованности и возможная согласованность не может быть вариантом, то это «правило», которое я готов согнуть и включить более одной совокупности в транзакцию. Примером может служить обработка транзакции журнала, когда сумма переводится с одного счета в моей системе на другой. Если у вас есть разные системы, то в конечном итоге потребуется согласованность, и для «отката» потребуются компенсирующие операции.

2 голосов
/ 21 февраля 2020

Хранение ссылки на другой агрегат (будь то «многие ко многим» или что-то еще) и обновление его в той же транзакции, фактически нарушает фундаментальный принцип проектирования агрегатов. Агрегат - это единица согласованности, соответствующая собственной границе согласованности. Предполагается, что транзакция обновляет и, следовательно, обеспечивает согласованность только одного агрегата.

Обновления между агрегатами, через границы согласованности, естественно необходимы. DDD рекомендуемый способ для такого рода обновлений - возможная согласованность : их последующее асинхронное обновление в другой транзакции. Агрегат ссылается на другой агрегат, удерживая его идентификатор , а не поле с отношением (многие-ко-многим в вашем случае). Всякий раз, когда необходимо обновить другой агрегат, оставьте событие domain , содержащее другой опубликованный идентификатор агрегата, до совершения текущей транзакции. Подписчик события домена получает событие асинхронно, извлекает агрегат с идентификатором, производит необходимое обновление и сохраняет его. Это примерно базовая c идея.

1 голос
/ 27 марта 2020

Прежде всего, я согласен с Eben: не используйте ссылки на объекты одного агрегата внутри другого, используйте вместо него объект-значение, просто содержащий идентификатор других агрегатов. И в базе данных этот идентификатор представляет собой просто строку или целое число (или то, что вы используете в качестве типа идентификатора в базе данных) вместо внешнего ключа.

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

В большинстве случаев получается просто передать требуемые данные, собранные из первого агрегата, в метод, вызываемый для нового агрегата. достаточно.

Если это даже происходит в одном и том же ограниченном контексте, я склонен быть прагматичным c по этому поводу. Я собираю агрегат, из которого мне нужны данные, через его репозиторий, а затем передаю его в качестве параметра методу нового агрегата. Или только какая-то его часть. Я обычно делаю это внутри службы приложений.

Таким образом, вам не нужно хранить какую-либо другую информацию о старом агрегате в новом, а не его идентификатор, но у вас всегда есть актуальное состояние старый агрегат, где вам это нужно. Эта концепция даже не относится к проектированию, основанному на предметной области, но в целом рекомендуется использовать только те зависимости, которые вам действительно нужны.

А если вы не хотите полагаться на структуру старого агрегата, просто создайте несколько вид нового объекта значения, который вы заполняете данными старого агрегата на прикладном уровне. Поэтому вам даже не нужно собирать данные из старого хранилища агрегатов, а просто есть какой-то сервис, который только считывает необходимые данные из хранилища напрямую. Но я бы порекомендовал это, только если проблема заключается в производительности ...

И еще один последний комментарий об использовании внешних ключей в базах данных в монолитных c приложениях:

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

...