问题描述
在 CQRS 中,当我们需要为我们的读取模型创建定制的投影时,我们通常更喜欢“非规范化”投影(假设我们正在谈论投影到数据库上)。应用程序/用户界面所需的信息来自不同的聚合体(可能来自不同的 BC)并不少见。
想象一下,我们需要一个投影表来包含客户的信息及其完整地址,并且 Customer
和 Address
在我们的系统中是不同的聚合(可能在不同的 BC 中)。这意味着,地址是独立于客户生成和维护的。或者,换句话说,当一个新客户被创建时,不能保证系统会产生一个AddressCreatedEvent
随后,这个事件可能在之前已经被处理了客户的创建。在 CreateCustomerCommand
时,我们所拥有的只是现有地址的 UUID。
-
丰富
CreateCustomerCommand
和随后的CustomerCreatedEvent
以包含客户的完整地址(从 UI 或控制器即时查找此信息)。这样,投影处理程序将在收到CustomerCreatedEvent
时直接更新表。 -
使用
addrUuid
中提供的CustomerCreatedEvent
在投影处理程序中执行临时查询以在更新表之前获取地址信息的缺失部分。
这些是此问题的常见讨论解决方案。然而,正如许多其他人所指出的,每种方法都存在问题。例如,如 Enrico Massone in this question 所描述的那样,丰富事件可能难以证明是合理的。查询其他视图/投影(一种 JOIN)会起作用,但会引入耦合(请参阅相同的链接)。
我想在这里描述另一种方法,据我所知,它很好地解决了这些问题。如果这是一种已知的技术,我事先为没有给予适当的信任而道歉。真诚地,我没有在其他地方看到它描述过(至少没有那么明确)。
“一张图片会说一千个字”,正如他们所说:
这个想法是:
- 我们保持
CreateCustomerCommand
和CustomerCreatedEvent
简单,只有addrUuid
属性(没有丰富)。 - 在 API 控制器中,我们向命令处理程序(聚合)发送两个命令:第一个,像往常一样,-
CreateCustomerCommand
与 {{1} 一起创建客户和项目客户信息}} 到表中,暂时将其他列(完整地址等)留空。 (警告:查看更新,我们这里可能有并发问题,需要从 Saga 发出探测命令。) - 紧接着,在我们获得新创建的客户的
addrUuid
之后,我们向custUuid
发出一个特殊的ProbeAddrressCommand
聚合触发一个Address
,它将封装地址的完整状态以及特殊属性AddressprobedEvent
,这当然是我们来自上一个命令的probeInitiatorUuid
。 - 然后,投影处理程序将通过简单地填充表中缺少的信息来对
custUuid
采取行动,通过匹配提供的AddressprobedEvent
(即probeInitiatorUuid
) 和custUuid
。
所以我们有两个阶段:创建 addrUuid
和探测相关的 Customer
。它们在图中分别用 (1) 和 (2) 表示。
显然,我们可以根据投影的需要发送尽可能多的这样的“探测”命令(并行):Address
、ProbeBillingCommand
等,有效地填充或“填充”非规范化的投影每个处理的“探测”事件丢失数据。
这种方法的优点是我们在第一阶段保持简单的命令/事件(只有 UUID 到其他聚合),同时避免投影的同步耦合(连接)。整个方法有一种很好的 EDA 感觉。
我的问题是:这是一种已知的技术吗?好像我没见过这个……这种方法有什么问题?
我很乐意通过引用描述此方法的其他来源来更新此问题。
更新 1:
我已经看到这种方法存在一个重大缺陷:命令 ProbePreferencesCommand
不能在投影处理程序有机会处理 ProbeAddrressCommand
之前发出。但这是不可能从 API 网关(或控制器)知道的。
解决方案可能涉及一个 Saga,比如 CustomerCreatedEvent
将在收到 CustomerAddressJoinProjectionSaga
时开始,然后才会发出 CustomerCreatedEvent
。 Saga 将在注册 ProbeAddrressCommand
后结束。或者,如果在探测中涉及许多其他聚合,则在接收到所有此类事件时。
这是更新后的图表。
更新 2:
正如 Levi Ramsey 所指出的(见下面的答案),我的例子在聚合体的选择方面相当复杂。实际上,AddressprobedEvent
和 Customer
通常被概念化为属于一起(相同的聚合根)。因此,考虑类似 Address
和 Student
之类的问题可以更好地说明问题,为简单起见,假设两者之间存在直接关系:学生正在学习一门课程。这样更明显的是,Course
和 Student
是独立的聚合体(学生和课程可以在系统的不同时间和不同位置创建和维护)。
但问题仍然存在:我们如何才能获得包含学生完整信息(全名等)和她注册的课程(职称、学分、教师全名、先决条件等)的投影? ) 都在同一个表中,如果 UI 需要它?
解决方法
一些想法:
鉴于客户有地址的要求,我质疑为什么地址需要在不同的有界上下文中更不用说是一个单独的聚合。如果在某些其他有界上下文中客户地址有意义(例如,您想知道“哪些地址有更多客户”等),则该上下文可以从客户服务订阅事件。
作为替代方案,如果有特别强烈的理由将地址与客户分开建模,为什么不让读取端前瞻性地侦听地址聚合中的事件并存储给定地址 UUID 的最新地址,以防万一有客户以该地址结束。我预计这种方法的单位工作可靠性可能会更高一些。