在领域驱动设计中,聚合是否应该保留来自另一个聚合的冗余或聚合数据以保持不变性?

In domain driven design, should an aggregate keeps redundant or aggregated data from another aggregate to maintain invariants?

在领域驱动设计中,一个聚合是否应该保留来自另一个聚合的冗余数据以保持不变性?

比如我们设计的系统有School和Student聚合根,有一条业务规则:一个学校的学生总数不能超过1000

在学校招收新生时,有两种方法可以强制执行规则:

  1. 对学生进行 select 计数并对照 1000 检查数字,可能需要按学校 ID 锁定

  2. 在School聚合中维护“student_count”属性,验证School域内的约束,先增加student_count,然后创建一个新的Student。删除学生(离开学校或毕业)时需要更新“student_count”属性。

实际上我更喜欢方法 2,因为它强制执行 School 域的不变量,但我的同事认为方法 1 易于编写和维护。

我正在寻找有关此主题的更多讨论。 谢谢

如果您保证一次只有一个客户端与服务交互,那么选项 1 将是最简单的检查。多用户环境中选项 1 的问题是在事务处理过程中对学校 table 的悲观并发锁定。

选项 2 允许您从学校集合中的数据库中检索当前计数,然后要求学校集合添加学生并增加计数,然后再保存回数据库。此时,学校聚合可以在添加学生之前检查计数,如果计数已经是 1,000,则抛出。

对于选项 2,我将在 School 上实现并发令牌,以便可以实现乐观并发。如果学校学生人数已被另一个进程更改,您将收到 DbConcurrencyUpdate 异常,您可以适当地处理该异常。

我会在应用层捕获它并代表客户端重试。如果第二次(或第三次......)你成功了,那么一切都很好。如果随后出现“学生太多”错误,则可以返回给客户端。

选项 2 绝对更符合 DDD - 在学校聚合而不是应用程序层中保持不变检查。

For example, if we are designing a system with School and Student aggregate roots, there is a business rule: the total number of students in a school must not exceed 1000

这个问题的总称是set-validation

如果“不得超过 1000”是一个硬约束(例如:如果发生这种情况,将会失去生命/我们可能会被起诉不复存在),那么答案是集合本身必须完全包含在单个锁,通常翻译为“该集合必须完全包含在一个聚合中”。

所以这可能意味着“学校”聚合包含一组学生 ID,当该集合包含 1000 个 ID 时,注册将关闭。

更常见的是集约束是软的(“当这种情况发生时,我们赚的钱更少”),在这种情况下,我们可以使用差异报告和升级来触发使集恢复合规性的域流程。


最近在 slack:ddd-cqrs-es 上讨论了这样一个事实,即我们用于“调度和计划”的约束通常比我们用于“操作”的约束更严格。

要认识到的一个关键区别是:在现实世界中运行时,现实世界是“记录簿”,我们的模型只是根据本地缓存的信息副本工作,这些副本可能是错误的或不正确的日期。我们必须谨慎对待阻止现实世界更正我们数据的设计。