通过 id 引用聚合根时使用外键

Using foreign keys when referencing aggregate roots by id

最佳做法是通过 ID 而不是引用来引用另一个聚合根(请参阅 "Implementing Domain Driven Design" 第 359 页及后续内容)。

// direct reference
class AggregateRootA(val b: AggregateRootB)

// reference by ID (preferred)
class AggregateRootA(val b: AggregateRootBId)

class AggregateRootB(val id: AggregateRootBId)
class AggregateRootBId(val id: Long)

这种解耦的核心论点是每个聚合根应该是一个事务边界。

我想知道在代码中使用id引用时在数据库中使用外键关系是好是坏。此约束将在数据库级别强制执行一致性,因为没有 AggregateRootB 的引用记录就不可能有 AggregateRootA 的数据库记录。这基本上就是我想要的,否则对象将无效。

除了测试稍微麻烦一些之外,这里使用外键还有什么缺点吗?

Is there any downside of using a foreign key here apart from slightly more cumbersome testing?

约束引入了一些时间耦合;除非外键实际可用,否则数据库将不允许写入,这在两者之间引入了 "happens-before" 关系。

因此,如果您有一个真正的业务问题,即关于 B 的簿记必须在将 A 链接到 B 的簿记之前发生,那么在数据库中强制执行该约束就可以了.

但这并不是一个普遍的问题,如果信息并不总是按自然顺序到达,外键约束可能会很痛苦。

简而言之,仔细衡量限制的成本和好处。

添加评论太长了,但我只是添加@VoiceOfUnreason 的回答,所以请接受那个答案,而不是这个:)

迂腐一下:您指的是 DRI(声明性参照完整性),因为甚至可以禁用外键关系。

DRI 本身没有任何问题,因为它有一个相当正当的目的。当然,它在同一个限界上下文中最有意义,因为数据将在同一个数据库中。对于不同的 BC,您可以完全放弃关系的执行并仅处理非规范化数据,或者通过将值对象存储在 BC 的数据存储中来执行关系。来自 "foreign" BC 的聚合将是 您的 BC 中的 VO。在这种情况下,@VoiceOfUnreason 提到的一切都会发挥作用。在某些情况下,确保某些状态更改以合理的顺序发生可能非常有意义。例如,您可能希望在激活 phone 行之前注册一个客户。

对于规则不太严格的情况,您可以继续建立弱关系,并可能通过指示相关数据是否正在等待其他位完成的状态来跟踪您的过程,这是典型的 asynchronous/parallel 处理范式。