【问题标题】:Split aggregate root to avoid concurrency conflicts拆分聚合根以避免并发冲突
【发布时间】:2024-04-12 10:40:01
【问题描述】:

阅读越来越多的聚合根,特别是来自 Vaughn Vernon 的 I-DDD,我提出了一个关于并发的问题。

多个用户同时访问一个系统。 (当前)核心域是关于教育中心的“注册”,因此有一个 Student 实体代表企业的主要客户。 Student 是一个聚合根,当然有很多信息。

假设Student 有一个PersonalAddressAcademicInformation(过去的学校、成绩等),它们都被建模为聚合 内的值对象 ,并且-为了论证-在同一个Aggregate中没有其他EntityStudent AggregateEntity 中使用类似于版本属性的乐观并发,因此对其数据的任何更改都会增加该版本。

问题是,如果两个不同的用户同时尝试修改同一个StudentPersonalAddressAcademicInformation,其中一个尝试将失败,即使地址和学术信息完全不相关;虽然两者都是VO,但它们属于同一个聚合

我认为我可以拆分 聚合 以避免并发冲突,因为除了“属于”同一个 Student 之外,没有与 PersonalAddressAcademicInformation 相关的真正不变量。但这些都是VO,没有自己的身份。我必须创建另一个 Entity 并将它们放在不同的 Aggregate Roots 中,两者都包含与同一学生相关的信息,因此可以同时修改这些信息。

所以问题是:

  1. 如何避免修改被建模为值对象PersonalAddressAcademicInformation)的同一实体Student)的无关信息的并发冲突)?
  2. 正如我之前解释的那样,将Student聚合拆分成两个或更多不同的聚合根是一种“好方法”吗?
  3. 即使这种特殊情况可以通过其他方式解决(如果能分享,我将不胜感激),如何从更一般的角度解决这个问题?

我认为这一切都取决于用户尝试同时修改信息的频率,并据此决定拆分聚合是否值得……但我不知道。

非常感谢。

【问题讨论】:

    标签: concurrency domain-driven-design aggregateroot


    【解决方案1】:

    1) 您可以通过其他方式使用乐观并发:基于旧值,而不是版本号。例如。更改学生的命令应该看起来像 new ChaneStudentCmd { StudentId = ...; OldAddressStreet = "xxx"; NewAddressStreet = "yyy" },实现应确保当前 strret 在更改之前为 "xxx"。如果不是“xxx”则应该抛出并发异常。

    2) 我认为没有理由拆分聚合。

    3) 一般方法可以使用更具体的更新命令,而不是简单的“更新所有学生属性”。业务层应该有关于该用户想要更新的确切信息。考虑到并发性和其他需求,有了这些信息,它将能够优雅地处理更新。

    【讨论】:

      【解决方案2】:

      在我看来,您可以保持原样,这是最简单的方法。但是,将不相关的属性或方法保留在一个聚合/实体/类中会破坏“高内聚”规则。 所以我想知道你的情况是否是一种未被发现的限界上下文的“气味”。 PersonalAddressAcademicInformation 是否属于同一个限界上下文?我不知道您的域和用例,但您应该考虑一下。

      回答您的问题:

      广告 1. 通过将不相关的信息分成不同的有界上下文或聚合来避免冲突。但是,相关信息仍然可能存在并发冲突。

      广告 2. 正确建模有界上下文和聚合是一种“好方法”(尽管这并不容易;))。所以并发的冲突和不相关的信息是“气味”,让你知道你错过了模型中的某些内容。

      广告 3。同样,正确的限界上下文和聚合建模。

      我并不是说在每种情况下都可以通过分离 BC 和聚合来避免遇到的情况,但我确实认为这是可能的。我并不是说,在一个聚合中没有不相关的属性和方法总是正确的,但这是“高内聚”规则引导我的地方。

      【讨论】: