【问题标题】:Rebuild queries from domain events by multiple aggregates通过多个聚合从域事件重建查询
【发布时间】:2014-12-05 21:55:53
【问题描述】:

我正在使用 DDD/CQRS/ES 方法,我对我的聚合和查询建模有一些疑问。例如,考虑以下场景:

用户可以创建一个 WorkItem、更改其标题并将其他用户关联到它。 WorkItem 有参与者(关联用户),参与者可以将操作添加到 WorkItem。参与者可以执行操作。

假设用户已经创建,我只需要 userIds。

我有以下 WorkItem 命令:

  • 创建工作项
  • 更改标题
  • 添加参与者
  • 添加操作
  • 执行操作

这些命令必须是幂等的,所以我不能添加两次相同的用户或操作。

还有以下查询:

  • WorkItemDetails(工作项的所有信息)

查询由处理由 WorkItem 聚合引发的域事件的处理程序更新(在它们被持久化到 EventStore 之后)。所有这些事件都包含 WorkItemId。如果需要,我希望能够通过加载所有相关事件并按顺序处理它们来即时重建查询。这是因为我的用户通常不会访问一年前创建的 WorkItem,因此我不需要处理这些查询。因此,当我获取一个不存在的查询时,我可以重建它并将其存储在具有 TTL 的键/值存储中。

域事件有一个aggregateId(用作事件streamId和shard key)和一个sequenceId(用作事件流中的eventId)。

所以我的第一次尝试是创建一个名为 WorkItem 的大型聚合,其中包含一组参与者和一组操作。 Participant 和 Actions 是仅存在于 WorkItem 中的实体。参与者引用用户 ID,操作引用参与者 ID。他们可以获得更多信息,但这与本练习无关。使用此解决方案,我的大型 WorkItem 聚合可以确保命令是幂等的,因为我可以验证我没有添加重复的参与者或操作,如果我想重建 WorkItemDetails 查询,我只需加载/处理给定的所有事件工作项 ID。

这很好用,因为我只有一个聚合,WorkItemId 可以是聚合Id,所以当我重建查询时,我只需加载给定 WorkItemId 的所有事件。 但是,此解决方案存在大型 Aggregate 的性能问题(为什么要加载所有参与者和操作来处理 ChangeTitle 命令?)。

所以我的下一个尝试是拥有不同的聚合,所有聚合都具有相同的 WorkItemId 作为属性,但只有 WorkItem 聚合具有它作为聚合 ID。这解决了性能问题,我可以更新查询,因为所有事件都包含 WorkItemId 但现在我的问题是我无法从头开始重建它,因为我不知道其他聚合的聚合 ID,所以我无法加载他们的事件流并处理它们。他们有一个 WorkItemId 属性,但这不是他们真正的 aggregateId。我也不能保证我按顺序处理事件,因为每个聚合都有自己的事件流,但我不确定这是否是一个真正的问题。

我能想到的另一个解决方案是有一个专用的事件流来整合由多个聚合引发的所有 WorkItem 事件。因此,我可以拥有事件处理程序,将参与者和操作触发的事件简单地附加到其 id 类似于“{workItemId}:allevents”的事件流中。这将仅用于重建 WorkItemDetails 查询。这听起来像是一个 hack.. 基本上我正在创建一个没有业务操作的“聚合”。

我还有哪些其他解决方案?即时重建查询是否不常见?当使用多个聚合(多个事件流)的事件来构建相同的查询时,可以这样做吗?我已经搜索了这种情况,但没有发现任何有用的东西。我觉得我错过了一些应该非常明显的东西,但我还没想清楚是什么。

非常感谢您对此的任何帮助。

谢谢

【问题讨论】:

  • 您能否进一步解释“我无法从头开始重建它,因为我不知道其他聚合的 aggregateId,因此我无法加载它们的事件流并进行处理”?是否可以在 ExcecuteActionCommand 中包含 actionId?添加操作后,可以通过 workItemId 查询来检索 actionId。还是我错过了什么?
  • 是的,从头开始重建 WorkItemDetails 将加载给定 WorkItemId 的所有事件(从事件存储中)。但是 Participant 和 Action 的事件存储在它们自己的事件流中,尽管事件的有效负载可能包含 WorkItemId。这就是为什么我想为 WorkItemDetails 提供一个专用的事件流,以便在重建时我只需在该事件流中加载事件。我不想执行多个查询(可能数百个)来从多个流(聚合)加载事件。
  • 对不起,我还是不明白。 WorkItemDetails 是聚合还是查询?如果是查询,是否存储为 WorkItem 的最新状态?
  • 这是一个查询,根据来自多个聚合的事件更新。我的观点是,当我需要从头开始重建时,我不知道要从哪个事件流中加载,因为我需要加载,例如,来自动作 1、2 等的事件,而且我不知道有多少将有的操作及其 ID。

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


【解决方案1】:

我认为您不应该在设计聚合时考虑到查询问题。阅读方面就是为了这个。

在域方面,关注一致性问题(聚合可以多小并且域在单个事务中仍然保持一致)、并发性(它可以有多大并且不会遇到并发访问问题/竞争条件?)和性能(我们是否会在内存中加载数千个对象只是为了执行一个简单的命令? -- 正是您要问的)。

我认为按需读取模型没有任何问题。它与从实时流中读取基本相同,只是您在需要时重新创建流。然而,这可能是相当多的工作,而不是非凡的收获,因为大多数时候,实体在修改后就被查询。如果按需变为“基本上每次实体更改时”,您还不如订阅实时更改。至于“旧”视图,“旧”的定义是它们不再被修改,因此无论如何都不需要重新计算它们,无论您是按需还是连续系统。

如果您采用多个小型聚合路线,并且您的读取模型需要来自多个来源的信息来更新自身,您有几个选择:

  • 使用附加数据丰富发出的事件

  • 从多个事件流中读取并整合其数据以构建读取模型。这里没有魔法,读取端需要知道特定投影中涉及哪些聚合。如果您知道其他读取模型是最新的,您也可以查询它们并且只会为您提供所需的数据。

CQRS events do not contain details needed for updating read model

【讨论】:

  • 我知道这里没有魔法,但设计聚合似乎是错误的,以便我可以确定性地重建查询,但如果我不能在任何给定时间拥有一致的视图在我的数据中,我怎么能说我的聚合是为一致性目的而精心设计的?也许我的 EventStore 中缺少高级功能,这些功能是来自其他事件流的投影的事件流,与聚合的设计方式无关。我是否正在尝试做一些在 CQRS 中不常见的事情?
  • 当每个事务使聚合处于不违反任何域不变量或规则的状态(除非您使用最终一致性)时,您可以说您的聚合设计得很好。观点是连贯的只是其副产品,而不是主要目标。此外,视图仅在给定时间点有效,基于视图显示的更改可能会被聚合拒绝,因为它们不再有效。
  • 我会让其他人回答这个问题,但是是的,根据我的经验,您尝试做的事情在 CQRS 中并不常见。
  • 我不确定这是否是一个常见的用例。我将尝试将我的聚合建模为列表。 WorkItemParticipants 和 WorkItemActions。这也将让我避免重复并有两个单独的查询,用于参与者和操作。 WorkItemDetails 查询可以从这两个中重建。同样,这只是从头开始重建时的问题。我会接受你的回答。如果其他人也能评论他们对这个特定用例的体验,那就太好了。
  • 丰富事件不是一个好主意。我很努力,但它是错误的,因为其他数据与聚合无关,甚至不在它的事务范围内,所以数据可能是错误的。你想买一辆法拉利,当你结账时,有人将法拉利的描述更新为土豆。发出事件“客户为一袋土豆支付了 300.000 美元”,然后修复了 Ferrari 描述,但您的不良事件将在那里......永远存在。最好只是发出:“客户为商品 5321234 支付了 300.000 美元”。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2018-12-17
  • 2019-12-04
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多