【问题标题】:Handling events in an cross-aggregate relationships, and aggregate state处理跨聚合关系和聚合状态中的事件
【发布时间】:2018-11-03 21:53:23
【问题描述】:

我最近开始了我第一次尝试使用领域驱动设计原则结合事件溯源和 CQRS 开发票务 Web 应用程序。

由于这是我第一次尝试摆脱传统的 CRUD 方法并进入 DDD 世界,我确信我有很多设计错误的东西,因为 DDD 需要付出很多努力才能提出正确的域分离和限界上下文等等

在我的设计中,我有接受命令、启动作业(工作单元)的命令处理程序,它们从聚合存储库加载所需的聚合(通过重播事件从事件存储加载聚合),并且它们通过每个聚合的公开操作来操作聚合,然后关闭作业。

聚合公开实际发出事件的操作。例如,company.Create(firmName, address, taxid, ...) 发出 CompanyCreated 事件,并将其应用于自身。当 Job 即将完成时,在该 Job 的上下文中加载的所有聚合中的所有事件都会被事件存储收集和保存。

现在,我遇到了一种情况,我确信这很常见,我在聚合之间存在关系。例如Customer 具有Contacts,或者SupportAgentDepartment 的成员。这些是我设计中的聚合。

让我们以Department 为例。 Department 的状态由标题、描述、一些其他属性以及属于该部门成员的那些代理的SupportAgent id 列表组成。 SupportAgent 的状态包括姓名、姓氏、电话号码、电子邮件……以及此代理所属部门的 Department id 列表。

现在,当处理AddAgentToDepartment(agentId, departmentId) 类型的命令时,会发出两个事件。为相应的代理发出DepartmentAdded,将部门id 添加到代理的状态,并为相应的部门发出SupportAgentAdded,将代理id 添加到部门的状态。

我的第一个问题是:将相关聚合的 id 保持在聚合状态中是否正确?“正确”是指最佳实践吗?还是有其他方法(例如,将关系保持在某种“DepartmentMemberManager”实体/聚合或其他东西中。实际上这个实体或这里的任何东西都是单例。在 DDD 世界中是否有这样的东西)?

我的另一个想法是关于事件回放。在前面的示例中,发出了两个事件,但是为了更新视图,只需要处理其中一个,因为这两个事件都描述了系统状态的完全相同的转换(代理和部门是链接的)。我选择只处理SupportAgentAdded 事件来更新视图。我的事件处理程序执行一个 SQL 脚本来更新相应的数据库表以反映系统的当前状态。

如果我们需要重放某些事件以使某个聚合的视图保持一致状态会发生什么?具体来说,当我想为支持代理重播事件时,只会重播 DepartmentAdded 事件,并且这些事件不会由任何人处理,因此不会更新视图。 为了使整个系统进入一致状态,是否应该部分重放事件存储中的某些事件或所有事件?

如果你是 DDD 和 ES 专家,或者至少你有经验,我想得到一些提示,告诉你我在做什么,或者在想什么,错了,我应该看什么方向。

【问题讨论】:

    标签: domain-driven-design aggregate cqrs event-sourcing aggregateroot


    【解决方案1】:

    CQRS 表示命令-查询职责分离。有两个方面 C - 命令,写入方面。 Q - 查询,读取端。

    聚合存在于 C - 命令端,并且只能执行一个命令。无法查询聚合。因此,在您的示例中,您的代理的命令处理程序根本无法与某些部门聚合

    虽然可以查询读取模型,所以没有什么可以阻止您查询某些部门读取模型。但是存在一致性问题。

    聚合实例根据其事件流是一致的,这意味着在您执行命令时没有任何东西可以更改此聚合的状态。因此,您的聚合是一个事务边界 - 其状态中的所有内容都是一致的,而其状态之外的所有内容 - 可能不一致。

    因此,如果您正在处理聚合状态之外的任何内容 - 您正在处理可能不一致的数据 - 在您的示例中,您的部门可能已经被删除,但读取模型尚未显示这一点。

    现在,聚合不是一个实体。 “聚合”这个名字意味着那里有几个“东西”。聚合是一个可以执行命令并确保业务规则的对象。这意味着命令被发送到一个聚合。

    选择聚合是 CQRS/ES 系统中的主要领域设计活动。错误是非常昂贵的,因为您需要处理事件版本控制和重构(Greg Young 最近写了a book 关于它)

    因此,在您的示例中,我们确实有一个命令:

    AddAgentToDepartment(agentId, departmentId)
    

    第一个问题 - 它针对的是哪个聚合?请记住 - 一个命令用于一个聚合。这是一个设计决策,取决于您的系统。我会想到这样的事情:如果没有这个命令,Agent 还能成为 Agent 吗?我猜是这样,明天您将没有部门,但是,例如,产品和代理不应该受到影响。部门可以是没有这个命令的部门吗?不太可能 - 将代理分组是一回事。所以我会让一个部门成为一个接收

    AddAgentToDepartment(departmentId, params: { agentIdToAdd })
    

    并且部门聚合将关心业务规则(不能两次添加相同的代理,不能删除不存在的代理等)

    请记住,您可以轻松地为代理创建一个读取模型,该模型列出给定代理的所有部门,您只是不需要代理聚合状态中的部门,因为您不会将与部门相关的命令发送给代理.

    如果所有与代理相关的命令都应该知道部门,您可以将代理设置为AddAgentToDepartment 的目标。并且部门聚合将具有最少的命令集:创建、重命名、删除。

    我的第一个问题是:将相关聚合的 id 保持在聚合的状态中是否正确?

    没有。命令被发送到单个聚合,并且命令处理程序只能处理从该聚合的事件流计算的聚合状态。保留其他聚合的 ID 无济于事,因为您不能在任何地方使用它们。

    我的另一个想法是关于事件回放。在前面的示例中,发出了两个事件,但为了更新视图,只需要处理其中一个,因为这两个事件都描述了系统状态的完全相同的转换(代理和部门链接)。

    您的事件流should make sence to a domain expert。在您的示例中,单个 AgentAddedToDepartment 事件是有意义的。两个事件 - 没有。在大多数情况下,单个命令应生成单个事件。

    如果我们需要重播某些事件以仅使某个聚合的视图处于一致状态,会发生什么情况?具体来说,当我想为支持代理重播事件时,只会重播 DepartmentAdded 事件,并且这些事件不会由任何人处理,因此不会更新视图。为了使整个系统进入一致的状态,是否应该部分重放事件存储中的某些事件或所有事件?

    看起来您混合了写入和读取方面。在一侧重放事件不应以任何方式影响另一侧。我们的reSolve 框架是这样工作的:

    在“C”-命令(写入)端,收到命令后,通过查询事件存储从该聚合的事件流中恢复聚合的状态:给我聚合 12345 的所有事件。

    在“Q” - 查询(读取)方面,没有聚合,有读取模型。这些读取模型通常由针对不同聚合的多种类型的事件构建。当您需要重建读取模型时 - 您正在查询事件存储:给我所有符合我的条件的事件。然后您将这些事件应用于读取模型(可能需要一些时间),当读取模型是最新的时,它可以订阅当前事件流并实时更新自身。

    【讨论】:

      【解决方案2】:

      在我的设计中,我有接受命令、启动作业(工作单元)的命令处理程序,它们从聚合存储库加载所需的聚合(通过重播事件从事件存储加载聚合),并且它们通过每个聚合的公开操作来操作聚合,然后关闭作业。

      您可能会对此有所回击。当聚合存储在不同的位置时,在单个事务(工作单元)中修改多个聚合变得非常复杂。如果一切都在“一个数据库”中,您可以摆脱它。但是一旦你引入了第二个数据库,你实际上就引入了一个“分布式事务”,处理起来要尴尬得多。

      在许多现代讨论中,基本假设是每个聚合都是“事务边界”,这意味着您只能在任何给定事务中修改单个聚合。这反过来意味着更加宽容的一致性约束——并且应该影响模型中多个聚合的单个“命令消息”最终可能会执行部分更新。

      如果我们需要重播某些事件以仅使某个聚合的视图处于一致状态,会发生什么情况?

      通常的答案是视图独立于聚合进行管理。无法保证每个聚合都会有一个视图(一些聚合可能没有自己的视图,而其他聚合可能不止一个)。

      它通常的工作方式是我们可以使用相关标识符(例如,聚合的标识符)来过滤事件流。因此,给定的读取模型不需要重播所有事件,只需重播事件的子集。

      为了使整个系统进入一致状态,是否应该部分重放事件存储中的某些事件或所有事件?

      马的课程 - 部分重播通常用于更新阅读模型。

      您可能会发现查看此2014 talk by Greg Young 很有用

      【讨论】:

        【解决方案3】:

        1) 我认为您的模型不模仿域。 例如: 您正在命名一个基于 CRUD 约定的命令 ('AddAgentToDepartment'),而不是一个业务域流程,在这种情况下可以将代理分配给一个部门或将一个部门分配给一个代理。

        2) 在这种情况下,谁是控制器/经理/看门人?分配代理时确保满足所有业务规则是否是部门的责任?或者,选择部门并确保其符合相关业务规则是否是代理的责任?

        3) 我建议重新考虑提出两个不同的事件?引发单个事件并创建一个跟踪代理 部门关系的投影可能很好。

        如果您需要预测代理和部门之间的多对多关联,这样您就可以轻松处理情况

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 2019-02-26
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2021-07-22
          • 2013-11-05
          相关资源
          最近更新 更多