【问题标题】:CQRS projections, joining data from different aggregates via probe commandsCQRS 投影,通过探测命令连接来自不同聚合的数据
【发布时间】:2021-05-19 13:01:44
【问题描述】:

在 CQRS 中,当我们需要为我们的读取模型创建定制的投影时,我们通常更喜欢“非规范化”投影(假设我们正在谈论投影到数据库上)。应用程序/UI 所需的信息来自不同的聚合体(可能来自不同的 BC)并不罕见。

假设我们需要一个投影表来包含客户的信息以及她的完整地址,并且CustomerAddress 是我们系统中的不同聚合(可能在不同的 BC 中)。这意味着,地址是独立于客户生成和维护的。或者换句话说,当一个新客户被创建时,并不能保证系统会产生一个AddressCreatedEvent随后,这个事件可能在创建之前已经被处理过了客户。在CreateCustomerCommand 时,我们所拥有的只是现有地址的 UUID。

我们在这里有几种解决方案。

  1. 丰富 CreateCustomerCommand 和随后的 CustomerCreatedEvent 以包含客户的完整地址(从 UI 或控制器即时查找此信息)。这样,投影处理程序将在收到CustomerCreatedEvent 后直接更新表。

  2. 使用CustomerCreatedEvent 中提供的addrUuid 在投影处理程序中执行临时查询,以在更新表之前获取地址信息的缺失部分。

这些是通常讨论的解决此问题的方法。然而,正如许多其他人所指出的,每种方法都存在问题。例如,Enrico Massone in this question 也描述了丰富事件的合理性。查询其他视图/投影(一种 JOIN)会起作用,但会引入耦合(参见相同的链接)。

我想在这里描述另一种方法,我相信它很好地解决了这些问题。如果这是一种已知技术,我事先为没有给予适当的信任而道歉。真诚地,我没有在其他地方看到它的描述(至少没有那么明确)。

正如他们所说:“一张图片说一千个单词”:

这个想法是:

  1. 我们将 CreateCustomerCommandCustomerCreatedEvent 保持简单,仅使用 addrUuid 属性(不丰富)。
  2. 在 API 控制器中,我们向命令处理程序(聚合)发送 两个命令:第一个,像往常一样 - CreateCustomerCommand 以创建客户和项目客户信息以及 addrUuid 到暂时将其他列(完整地址等)留空的表。 (警告:查看更新,我们可能在此处遇到并发问题,需要从 Saga 发出探测命令。)
  3. 紧接着,在我们获得新创建客户的custUuid 之后,我们向Address 发出一个特殊的ProbeAddrressCommand 聚合,触发一个AddressProbedEvent,它将封装地址的完整状态以及特殊属性probeInitiatorUuid,当然是我们之前命令中的custUuid
  4. 然后,投影处理程序将对AddressProbedEvent 采取行动,只需填写表中缺少的信息片段,通过匹配提供的probeInitiatorUuid(即custUuid)查找所需的行) 和addrUuid

所以我们有两个阶段:创建Customer 并探测相关的Address。它们在图中分别用 (1) 和 (2) 进行了描述。

显然,我们可以根据投影的需要发送尽可能多的“探测”命令(并行):ProbeBillingCommandProbePreferencesCommand 等。有效地填充或“填充”非规范化投影,其中缺少每个数据处理“探测”事件。

这种方法的优点是我们保持第一阶段的命令/事件简单(只有 UUID 到其他聚合),同时避免投影的同步耦合(连接)。整个方法给人一种很好的 EDA 感觉。

那么我的问题是:这是一种已知的技术吗?好像我还没有看到这个......这种方法会出现什么问题?

我会更乐意使用对描述此方法的其他来源的任何引用来更新此问题。

更新 1

我已经看到这种方法存在一个重大缺陷:命令ProbeAddrressCommand 在投影处理程序有机会处理CustomerCreatedEvent 之前无法发出。但这不可能从 API 网关(或控制器)得知。

解决方案可能会涉及一个 Saga,比如 CustomerAddressJoinProjectionSaga,它将在收到 CustomerCreatedEvent 时开始,然后才会发出 ProbeAddrressCommand。 Saga 将在注册AddressProbedEvent 时结束。或者,如果在探测中涉及到许多其他聚合,则在收到所有此类事件时。

这是更新后的图表。

更新 2

正如 Levi Ramsey 所指出的(请参阅下面的答案),我的示例在聚合的选择方面相当复杂。事实上,CustomerAddress 通常被概念化为属于一起(相同的聚合根)。因此,考虑类似StudentCourse 这样的问题可以更好地说明问题,为了简单起见,假设两者之间存在直接关系:学生正在学习课程。这样更明显StudentCourse是独立的聚合体(学生和课程可以在系统的不同时间和不同地点创建和维护)。

但问题仍然存在:我们如何获得包含有关学生的完整信息(全名等)和她注册的课程(职称、学分、教师全名、先决条件等)的投影。 ) 都在同一个表中,如果 UI 需要的话?

【问题讨论】:

    标签: cqrs saga event-driven-design


    【解决方案1】:

    一些想法:

    鉴于客户拥有地址的要求,我质疑为什么在不同的有界上下文中地址需要是一个单独的聚合更不用说。如果在其他一些有界上下文中客户地址是有意义的(例如,您想知道“哪些地址有更多客户”等),那么该上下文可以订阅来自客户服务的事件。

    作为替代方案,如果有特别充分的理由将地址与客户分开建模,为什么不让读取端前瞻性地从地址聚合中侦听事件并存储给定地址 UUID 的最新地址,以防客户以该地址结束。我预计这种方法的每单位工作量的可靠性可能会更高一些。

    【讨论】:

    • 感谢您的意见。请参阅关于聚合选择的更新 (2)... 这并不重要,因为组合来自 任何两个聚合 的数据(在投影中)是 CQRS 中的一项常见任务。据我所知,这些预测的存在是为了为 UI 提供方便、即用型的支持。如果 UI 要求单个表格,其中每一行的格式为:custUuidcustNameaddrUuidaddrFull(并且不能对相关事件的顺序做出任何假设,即CustCreatedEvt、@ 987654326@),我们该怎么做呢。
    • 维护所有地址的读取模型的问题(这样我们就可以在投影处理程序对CustCreatedEvt做出反应时查找完整地址)是我们必须将两个投影联系在一起:CustAddrProjTable,我们实际需要的一个,以及支持AddrProjTable,它只是为了提供addrFull。我们必须先从后者做一个SELECT addrFull,然后对前者做一个INSERT。我们应该避免像这样不同的预测加入,因为它们需要保持独立以促进重建。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2022-03-21
    • 1970-01-01
    • 2023-03-23
    • 1970-01-01
    相关资源
    最近更新 更多