事件溯源:如何将 aggregateRoot 转换为另一个

Event sourcing : how to convert an aggregateRoot into another one

基本上,问题是:

how to correctly build an event storage for an event sourced system that should be able to :

  • convert an aggregate into another one,

  • keep the same Id,

  • and still be able to reconstitute it from the event stream?

现在我的例子:

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

ProspectiveCustomer::convertToPayingCustomer(ProspectiveCustomerId $id)

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

所以现在设想以下事件流

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

让我们关注点 4) :

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

  1. 从其事件历史重构 PayingCustomer
  2. 致电 PayingCustomer::pay(金额 $amount)

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

EventStorage 服务将:

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

事件的堆栈现在包含:

commandHandler 如何处理 PayingCustomer::reconstituteFromHistory(EventsHistory $events) 而 $events 是从 ProspectiveCustomer

发出/适用于

的事件

编辑

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

但考虑到:

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

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

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

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

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

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

这可能是流程:

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

ProspectiveCustomerId 包含在 PayingCustomerCreatedEvent 中可能是个好主意,这样您就可以跟踪 PayingCustomer 回到 ProspectiveCustomer

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

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

  1. 已创建潜在客户
  2. 已接受报价
  3. 从潜在客户创建的付费客户
  4. 潜在客户已删除(或标记为 "converted",或已停用)

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

您需要执行 (3) 和 (4) 的域事件处理程序,因为您说的是相同的限界上下文。

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

但是,您开始假设您总是希望有一个潜在客户使用相同的身份来检索您的历史记录或其他内容。由于此假设是隐含的,因此很容易被遗忘,并且通常在 DDD 中尤其不鼓励(记住将隐含的事物明确化)。您可以轻松地在新的付费客户中保留对潜在客户的 ID 参考,然后就可以了。