处理跨聚合关系中的事件和聚合状态

Tha*_*dis 4 domain-driven-design aggregate aggregateroot cqrs event-sourcing

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

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

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

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

现在,我遇到了一种情况,我确信这种情况很常见,在那里我有聚合之间的关系。例如 a Customerhas Contacts,或者 aSupportAgent是a的成员Department。这些是我设计中的聚合。

让我们Department举个例子。ADepartment的状态由标题、描述、一些其他属性以及作为SupportAgent该部门成员的代理的id列表组成。ASupportAgent的状态由姓名、姓氏、电话号码、电子邮件等组成,以及Department该代理所属部门的ID列表。

现在,当AddAgentToDepartment(agentId, departmentId)处理类型为Command 时,会发出两个事件。一个DepartmentAdded是发出将增加部门ID为座席的状态相应的代理,并且SupportAgentAdded发出了相应的部门,将增加代理ID进入部门的状态。

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

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

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

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

Rom*_*min 5

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

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

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

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

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

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

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

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

AddAgentToDepartment(agentId, departmentId)
Run Code Online (Sandbox Code Playgroud)

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

AddAgentToDepartment(departmentId, params: { agentIdToAdd })
Run Code Online (Sandbox Code Playgroud)

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

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

如果所有与 Agent 相关的命令都应该知道部门,则可以将 Agent 设为AddAgentToDepartment. 部门聚合将具有最少的命令集:创建、重命名、删除。

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

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

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

您的事件流应该对领域专家有意义。在您的示例中,单个AgentAddedToDepartment事件是有意义的。两个事件 - 没有。在大多数情况下,单个命令应生成单个事件。

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

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

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

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