以交叉聚合关系和聚合状态处理事件

时间:2018-10-26 07:19:50

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

最近,我开始了我的第一个工作,即使用域驱动的设计原则,事件源和CQRS来开发票务Web应用程序。

由于这是我第一次尝试脱离传统的CRUD方法,而是进入DDD世界,因此我确信我做错了很多事情,因为DDD需要付出很大的努力才能得出正确的域,有限上下文的分离等

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

聚合显示实际发出事件的操作。例如,company.Create(firmName, address, taxid, ...)发出一个CompanyCreated事件,并将其应用于自身。当作业即将完成时,事件存储将收集并保留来自该作业上下文中加载的所有聚合中的所有事件。

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

让我们以Department为例。 Department的状态由该部门成员的代理的标题,描述,其他一些属性以及SupportAgent ID列表组成。 SupportAgent的状态由姓名,姓氏,电话号码,电子邮件等组成,该代理人所属的部门的Department ID列表也包含在内。

现在,当处理类型为AddAgentToDepartment(agentId, departmentId)的Command时,将发出两个事件。为相应的座席发出DepartmentAdded,将部门编号添加到座席状态;为相应的座席发出SupportAgentAdded,将代理编号添加到座席状态。

我的第一个问题是:将相关聚合的ID保持在聚合状态是否正确?“正确”是指最佳做法吗?还是有另一种方式(例如,将关系保留在一种'DepartmentMemberManager'实体/集合或某种东西中。实际上,此实体或此处的某种单例。在DDD世界中是否存在这种东西)?

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

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

如果您是DDD和ES方面的专家,或者至少具有经验,我想获得一些提示,告诉您您可能会看到我在做什么,在想什么,错了什么以及应该看什么方向。

3 个答案:

答案 0 :(得分:1)

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

聚合位于C-命令端,并且只能执行命令。无法查询聚合。因此,在您的示例中,您的Agent的命令处理程序根本无法与某些部门汇总进行对话

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

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

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

现在,聚合不是实体。非常“聚集”的名称意味着那里存在多个“事物”。聚合是可以执行命令并确保业务规则的对象。这意味着将命令发送到一个聚合。

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

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

AddAgentToDepartment(agentId, departmentId)

第一个问题-涉及哪个汇总?记住-一个命令一个聚合。这是一个设计决定,取决于您的系统。我会想到这样的事情:如果没有此命令,Agent仍然可以成为Agent吗?我想是的,明天您将没有部门,但是,例如,产品和代理不应受到影响。部门可以是没有此命令的部门吗?不太可能-将代理商分组是一件事情。因此,我将部门汇总为

AddAgentToDepartment(departmentId, params: { agentIdToAdd })

部门汇总将处理业务规则(不能两次添加同一座席,不能删除不存在的座席等)

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

如果所有与代理相关的命令都应通知部门,则可以将代理作为AddAgentToDepartment的目标。而且Department集合将具有最少的命令集:创建,重命名,删除。

  

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

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

  

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

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

  

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

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

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

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

答案 1 :(得分:1)

  

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

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

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

  

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

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

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

  

部分重播某些事件是正确的还是应该重播事件存储中的所有事件以使整个系统进入一致状态?

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

您可能会发现复习此2014 talk by Greg Young

非常有用

答案 2 :(得分:0)

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

2)在这种情况下,谁是控制器/经理/网关守卫?指派代理人时,确保所有业务规则均得到满足是部门的责任吗?还是选择部门并确保其满足相关的业务规则是代理商的责任?

3)我建议重新考虑提出两个不同的事件?引发单个事件并创建一个跟踪代理<->部门关系的预测可能就好了。

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