DDD:两个不同聚合之间的多对多关系

Jac*_*ian 5 architecture domain-driven-design

我有两个“大”实体或聚合,它们有自己的业务逻辑 - 它们在单独的事务中保存、更新和销毁。它们有自己的子实体,这些子实体通过这些聚合根进行操作。但问题是,这两个聚合彼此之间必须是多对多的关系。从用户界面的角度来看,有一种 UI,其中第二个聚合的一个现有实例被添加到第一个聚合。就数据库而言,有一个表保存第一和第二聚合表的外键

entity_one_id | entity_two_id
1             | 2
1             | 3
1             | 4
Run Code Online (Sandbox Code Playgroud)

在上面的示例中,第一个聚合的实例保存对第二个聚合的引用。

我的问题是,从领域驱动设计的角度来看,如果在保存第一个聚合时加载第二个聚合的实例并将其添加到第一个聚合中,可以吗?在伪代码中它可能看起来像:

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);
Run Code Online (Sandbox Code Playgroud)

看起来在这笔交易中我没有更改第二个聚合,而只更改了一个聚合。但我不确定 DDD 理论是否允许在保存一个聚合时加载多个不同的聚合。那么,这样的代码是否违反了理论呢?

Ebe*_*oux 9

聚合根不应包含其他聚合根的实例。例如,当调用一个方法时,聚合可能会向另一个聚合传递瞬态引用,但它不会保留该引用。仅在通话中使用。

你的例子实际上比你想象的更常见。如果我们必须更改为OrderProduct聚合,我们就有了多对多的关系。AnOrderItem代表这种关系,最好定义为值对象。

当您发现需要“引用”另一个聚合时,请只使用 id 或至少包含另一个聚合 id 的某个值对象。

我对交易的看法略有不同。聚合根是一致性边界,因此非常适合事务边界。应尽一切努力保持事务中的单一聚合,但您也需要务实。如果您需要高水平的一致性,但最终一致性可能不是一种选择,那么我愿意改变这一“规则”并在事务中包含多个聚合。一个例子可能是处理日记交易,其中金额从我的系统内的一个帐户转移到另一个帐户。当您拥有不同的系统时,则必须实现最终一致性,并且“回滚”将需要补偿操作。

  • 不完全的。在 ER 术语中,“OrderItem”被称为“关联实体”,它在 DDD 中作为 VO 实现得非常好。您通常会发现关系的“一方”更自然,而 AR 将是所有者。在“Order”<->“Product”关系中,*链接*表“OrderItem”更接近“Order”,这就是它构成该聚合的一部分的原因。可能是因为订单包含相对较少的产品,而产品可能包含在更多订单中。“OrderItem”可以与“OrderId”一起存储,但该对象不需要它,因为它包含在订单中。 (2认同)

afh*_*afh 6

首先,我同意 Eben 的观点,不要在另一个聚合中拥有一个聚合的对象引用,而是使用仅保存其他聚合 id 的值对象。在数据库中,这个 id 只是一个字符串或整数(或者您在数据库中用作 id 类型的任何内容)而不是外键。

并始终问自己,您真正需要其他聚合的哪些数据,以及对于新聚合的哪些操作,您到底需要什么样的数据?

在大多数情况下,事实证明,只需将从第一个聚合收集的所需数据传递到新聚合上调用的方法就足够了。

如果这种情况发生在相同的有界上下文中,我倾向于采取务实的态度。我通过其存储库收集我需要的数据的聚合,然后将其作为参数传递给新聚合的方法。或者只是其中的一部分。我通常在应用程序服务中执行此操作。

这样一来,您不需要在新聚合中保存旧聚合的任何其他信息(而不是它的 id),但无论您需要什么,您始终都可以拥有旧聚合的最新状态。这个概念甚至与领域驱动设计无关,但一般最佳实践是,仅在真正需要的地方使用依赖项。

如果您不想依赖旧聚合的结构,只需创建某种新的值对象,并在应用程序层中使用旧聚合的数据进行填充。因此,您甚至不需要从旧聚合的存储库中收集数据,而只需提供一些仅直接从存储中读取所需数据的服务。但如果性能是您的问题,我只会推荐这个......

关于在整体应用程序中的数据库中使用外键的最后一条评论:

如果您打算在某个时候拆分整体,那么如果您引用另一个有界上下文中的某些内容,请不要使用外键。使用逻辑引用,而不是将其视为某种远程 ID,并在应用程序层解析它们。否则,将您想要从单体应用中提取的不同服务的数据库分开可能会成为一场噩梦。