【问题标题】:Event sourcing : how to convert an aggregateRoot into another one事件溯源:如何将聚合根转换为另一个
【发布时间】:2016-03-08 05:11:00
【问题描述】:

基本上,问题是:

如何正确构建事件源系统的事件存储,应该能够:

  • 将一个聚合转换为另一个,

  • 保持相同的ID,

  • 并且仍然能够从事件流中重构它?

现在是我的例子:

我有一个ProspectiveCustomer,可以像这样转换为PayingCustomer

ProspectiveCustomer::convertToPayingCustomer(ProspectiveCustomerId $id)

PayingCustomer 将保持相同的 Id,以便跟踪其生命周期。

所以现在想象一下以下事件流

  1. ProspectiveCustomer 已添加到 CRM 中
  2. 向潜在客户提出了要约
  3. ProspectiveCustomer 接受了报价,因此转换为 PayingCustomer
  4. PayingCustomer 支付了账单

让我们专注于第 4 点):

我们将有一个 commandHandler 接收一个 paymentCommand {customerId:"123", amount:"500€"}。 它的工作是:

  1. 根据事件历史重构 PayingCustomer
  2. 致电 PayingCustomer::pay(Money $amount)

我的问题是关于 1) 从历史中重构:

EventStorage 服务将:

  1. 查找 AggregateId
  2. 加载事件(SELECT * FROM Events WHERE ID = 'xxx')

事件堆栈现在将包含:

  • 已添加潜在客户
  • ProspectiveCustomerWasMadeAnOffer
  • ProspectiveCustomerAcceptedTheOffer

commandHandler 如何处理PayingCustomer::reconstituteFromHistory(EventsHistory $events) 而$events 是从ProspectiveCustomer 发出/可应用于ProspectiveCustomer 的事件

编辑

目前我正在处理 PayingCustomer 拥有自己的 Id,但持有对 ProspectiveCustomerId 的引用的问题。

但考虑到:

  1. 这是相同的有界上下文
  2. 非常同一客户的生命周期(ProspectiveCustomer 在 PayingCustomer 开始时结束)

感觉有点乱,因为模型现在被 2 个 Id 污染了,而一个应该就足够了。

如果不是事件源系统,我肯定会选择一个唯一的 ID。

话虽如此,考虑到事件溯源只是一个实现细节,我正在寻找一种方法让两个聚合保持相同的 ID。

【问题讨论】:

  • 当然你会有一个ProspectiveCustomerConvertedToPayingCustomer 事件。并且将其应用于 PropsectiveCustomer 并返回 PayingCustomer 是没有问题的
  • 但问题是事件流转换后的重构
  • 我认为“付费客户”和“潜在客户”是“状态”而不是实体。这就是我误解的地方。很抱歉造成混乱。
  • 感谢您的关注。潜在客户可能具有与付费客户完全不同的不变量,因此需要将大客户聚合分解为 2 个更小但更精确的 AR
  • 你不转换,你创建一个新的聚合。当您创建一个聚合时,无论如何您都会使用一个外部 id,它是在事件中提供给您的。您复制您需要在付费客户汇总中拥有的所有潜在客户汇总详细信息。用两个词——没有聚合转化,只是正常的业务流程。

标签: php cqrs event-sourcing aggregateroot


【解决方案1】:

您有两个聚合,但没有“转换”。您踏上了一条危险的道路,这可能会导致您将购物车转换为订单(例如)。

您已经有了两个不同的概念 - 潜在客户和付费客户。他们可能是在您与领域专家的对话中发现的。这显然意味着两个聚合,有时是两个有界上下文。您不应该进行任何转换,但您绝对可以创建新的聚合来对系统中发生的事情做出反应(接受订单)。

  1. 已创建潜在客户
  2. 接受报价
  3. 从潜在客户创建的付费客户
  4. 潜在客户已移除(或标记为“已转换”或已停用)

我还希望“转换”一词来自领域专家。这是正常的,因为在销售中,他们使用此术语来表示感兴趣的人实际进行了购买。他们确实将其称为“转换”,您可以通过使用“3. 潜在客户 converted”将其包含在您的通用语言中,但这与 技术 转换无关, 表示改变对象类型。

您需要能够执行 (3) 和 (4) 的域事件处理程序,因为您说这是同一个有界上下文。

聚合唯一标识生成不是聚合本身的功能,它是在外部完成的,并且聚合在作为工厂方法或/和构造函数参数创建时获取其标识。因此,当您从潜在客户创建付费客户时,没有什么能阻止您使用相同的身份。

但是,您开始假设您总是希望拥有潜在客户,以便使用相同的身份检索您的历史记录或其他内容。由于这个假设是隐含的,它很容易被遗忘,并且通常在 DDD 中尤其不鼓励(记住将隐含的东西显式化)。您可以轻松地在新的付费客户中保留对潜在客户的 id 引用,然后就可以了。

【讨论】:

  • >> 你开始假设你总是希望有一个潜在客户来检索你的历史或其他东西,|||不,我没有
  • >>您可以轻松地在新的付费客户中保留对潜在客户的 id 引用 ||正如我的编辑中所述,这就是我目前正在做的事情。但感觉很乱(见编辑)。同样,如果我没有包含两个不同聚合事件的事件堆栈的问题,我会保留 ID。这让我们回到了我对上述答案的最后 3 个 cmets
  • 正如我在回答中提到的,我强烈建议在不做任何假设的情况下使用 PayingCustomer 对 ProspectiveCustomer 的身份引用。
  • 这是我目前正在做的事情(见编辑),但这只是因为我正在使用事件溯源。如果我有一个常规的状态持久系统,我会保留相同的 ID。为什么要我的存储定义我的建模?
  • 好吧,我已经回答过了。我不确定您是否坚持重复使用 id 的想法。您正在尝试制作当前显式、隐式的事物。这不是改进。我没有在我的回答中提到事件溯源,无论如何我都会这样做,无论我如何坚持我的聚合。
【解决方案2】:

我的第一个想法是它是相同的聚合但具有不同的状态。但是,正如问题的 cmets 所述,您需要它们是两个不同的聚合。

当你转换你的聚合时,你真正要做的是创建一个新的聚合,所以我会用域事件处理程序来解决这个问题。域事件处理程序将对事件做出反应并发出命令,因此让您的 ProspectiveCustomer 调度类似于 OfferAcceptedEvent 的事件处理程序可以执行的操作。

这可能是流程:

  1. 用户接受报价,ProspectiveCustomer 发送 OfferAcceptedEvent
  2. 事件处理程序对OfferAcceptedEvent 做出反应并分派CreatePayingCustomerCommand。 (OfferAcceptedEvent 应该包含来自ProspectiveCustomer 创建命令所需的所有数据)
  3. PayingCustomer 已创建

ProspectiveCustomerId 包含在PayingCustomerCreatedEvent 中可能是个好主意,这样您就可以将PayingCustomer 追溯到ProspectiveCustomer

【讨论】:

  • 所以简而言之,您不会保留相同的 Id,而是让 PayingCustomer 持有对 ProspectiveCustomerId 的引用。我说得对吗?
  • 是的,原始ID属于ProspectiveCustomer。 PayingCustomer 应该有自己的 ID。
  • 这就是我目前正在做的事情,但感觉很乱。考虑到这是同一个有界上下文,而且这是同一个 peron,感觉应该保持相同的 Id,因为它们每个都有自己的生命周期,但一个在另一个开始时结束。
  • Saga 协调顺序操作,流程管理器执行长时间运行的流程。您无需 saga 即可订阅一个域事件并发出命令。常规的域事件处理程序就可以了。
  • 这里kellabyte.com/2012/05/30/clarifying-the-saga-pattern 对工作流、状态机、saga 和流程管理器有一个很好的概述。 Saga 是另一回事,它以特定顺序执行命令,并在需要时提供补偿操作以回滚整个序列。域事件处理程序不会做这样的事情,我不知道为什么要在简单的事情上应用复杂的模式。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2018-12-28
  • 2023-03-14
  • 1970-01-01
  • 2018-10-03
  • 1970-01-01
相关资源
最近更新 更多