【问题标题】:DDD and CQRS: use multiple repositories from a single command handler?DDD 和 CQRS:使用来自单个命令处理程序的多个存储库?
【发布时间】:2021-10-23 16:55:34
【问题描述】:

简单电子商店的典型示例。

假设用户将一些商品添加到购物篮并点击“结帐”。发出“创建订单”命令。现在,在实际创建状态为“Payment Expected”的订单记录和数据库中的相应订单行之前,我们必须检查用户选择的商品是否仍然可用(也许当用户将某些商品添加到购物篮时它们是可用的但不再)。而且我们还必须保留它们,这样它们就不会在用户还在结账时突然消失。

所以我的问题是如何执行这个“检查和保留”例程?在我看来,我有多种选择:

  • 在“创建订单”命令处理程序中使用ProductStockRepository 保留产品,然后在成功使用OrderRepository 创建订单。这意味着,我们在单个处理程序中使用多个存储库。
  • 不要在“创建订单”处理程序中直接使用ProductStockRepository,而是创建一个ProductStockService 并调用其上的方法来检查和保留产品。我们仍然在单个处理程序中使用多个存储库,但库存存储库的使用是抽象的。
  • 创建一个内部“Reserve Products”命令并从“Create Order”命令处理程序内部调度并等待它。
  • “结帐”按钮发送“预订产品”命令而不是“创建订单”。在“保留产品”处理程序中,我们尝试保留产品并成功调用“保留产品”域事件。触发相应的事件处理程序,我们在其中创建一个订单。
  • 其他方式?

这不是关于如何最好地为电子商店结账流程建模的问题。以上只是一个例子。我想在许多不同的应用程序中可能会有许多类似的场景。

【问题讨论】:

  • 我知道您想要对您的场景进行回答,但我想指出,这不是简单的电子商店的运作方式。在成功下订单并付款之前,(几乎)从未保留库存。考虑到很大一部分在线支付失败。如果您保留库存(可能价值数千美元,具体取决于您出售的商品),您可能会因为库存不足而失去合法客户。此外,您的软件中的库存水平很可能不正确,因为仓库中的真实物品会丢失、损坏等,因此您无论如何都必须考虑下没有库存的订单。
  • 我不是真的写网店,只是大家用的一个例子。我只是想问一个关于具体事物而不是抽象事物的问题。我真正的问题是是否可以在单个命令处理程序中使用多个存储库,或者您必须一直使用事件。我认为在这种情况下,使用事件没有意义,但我不断看到有关事件如何始终是可行的方法的帖子。
  • 这不会回答您的问题,但它是一种启发式方法。事件溯源倾向于迫使周围的一切也使用事件溯源。诀窍是只在有意义的地方使用它,并在其他地方(例如经典的 DDD)使用更传统、更简单、易于进化的方法。除非您从一开始就拥有正确的聚合和事件集,否则事件溯源系统很难发展。如果有人正在重建 2/3 次相同的系统,则可能会出现这种情况。过去几年有一些论文证实了这一点,所以在使用 CQRS/ES 时请谨慎行事。
  • @Augusto 使用事件并不一定意味着使用事件溯源。我的看法是,大多数事件驱动架构不使用事件溯源,尽管我没有数据支持。
  • @FrancescCastells 你说的很对!我阅读了 CQRS 并假设它正在使用事件溯源。另一个例子说明假设有多糟糕:)

标签: domain-driven-design cqrs


【解决方案1】:

您提出的问题的解决方案与编码“风格”或遵循良好的 DDD 实践无关。如果在单个处理程序中使用多个存储库可以解决您的问题,我相信您应该将其视为一个不错的选择。

但这种场景的主要问题是,在许多系统中,订单和库存位于不同的服务/有界上下文中,因此位于不同的数据库中。库存甚至可能位于不受您控制的外部系统中。这意味着您无法保留库存并以交易方式下订单,因此您有保留库存而不下订单或以其他方式下订单的风险。

之所以建议使用事件来处理这些场景,是因为使用事件可以可靠地开发这种类型的工作流,尽管这会带来新的复杂性。借助一些技术,可以可靠地保留库存并发布事件,另一方面,可靠地捕获付款并发布另一个事件,然后下订单并发布另一个事件等。此工作流程可能涉及诸如发件箱模式、重试、sagas、补偿操作(在一个步骤失败的情况下回滚之前的步骤)等。

【讨论】:

    【解决方案2】:

    正如其他答案中提到的,您不希望您的 CommandHandler 维护多个聚合。这应该委托给 DomainService,通过 DomainEvents 实现,或者将 Products 传递到 Order 聚合中进行维护。解决方案还取决于预订流程和订单流程是否处于相同的有界上下文中。

    在同一 BC 中预订和订购,选项 1(域服务):

    • 已调度 CreateOrderCommand
      • 基础设施开始事务
        • CreateOrderCommandHandler 调用 CreateOrderDomainService
            1. CreateOrderDomainService 从存储库中检索产品并尝试保留,失败时抛出
            1. CreateOrderDomainService 尝试创建订单并添加到存储库,失败时抛出
      • 如果没有错误,Infrastructure 会提交。
      • 如果出现错误,基础架构将中止。

    同一 BC 中的预订和订购,选项 2(域事件):

    • 已调度 CreateOrderCommand
      • 基础设施开始事务
        • CreateOrderCommandHandler 创建订单并添加到存储库
        • 订单创建域事件“OrderCreated”
        • OrderCreatedEventHandler 从存储库中检索产品并尝试保留
      • 如果没有错误,基础架构会提交。
      • 如果出现错误,基础架构将中止。

    在同一 BC 中预订和订购,选项 3(注入产品):

    • 已调度 CreateOrderCommand
      • 基础设施开始事务
        • CreateOrderCommandHandler 检索按顺序使用的产品
        • CreateOrderCommandHandler 创建订单传入所有产品
        • 订单尝试使用产品域实体保留产品,抛出失败
      • 如果没有错误,基础架构会提交。
      • 如果出现错误,基础架构将中止。

    不同 BC 中的预订和订购,选项 1(域服务):

    • 已调度 CreateOrderCommand
      • 基础设施开始事务
        • CreateOrderCommandHandler 调用 CreateOrderDomainService
            1. CreateOrderDomainService 将 ReserveProduct 命令分派到 Reservations BC 并等待。
            • Reservations BC 完成预订,失败时抛出
            1. CreateOrderDomainService 尝试创建订单并添加到存储库
            • 如果失败,则通过向 Reservations BC 发送 UnreserveProduct 命令进行补偿,然后抛出。
      • 如果没有错误,Infrastructure 会提交。
      • 如果出现错误,基础架构将中止。

    不同 BC 中的预订和订购,选项 2(域事件):

    • 已调度 CreateOrderCommand
      • 基础设施开始事务
        • CreateOrderCommandHandler 创建订单并添加到存储库
        • 订单创建域事件“OrderCreated”
        • OrderCreatedEventHandler 将 ReserveProduct 命令分派到 Reservations BC 并等待。
          • 如果失败,则抛出。
      • 如果没有错误,基础架构会提交。
      • 如果出现错误,基础架构将中止。

    在所有情况下,在产品上使用并发令牌来防止并发“过度预留”。

    【讨论】:

    • 在Reservation和Order在不同BC的场景下,你建议使用分布式事务吗?如果这些 BC 使用不支持分布式事务的技术进行编码会怎样?
    • 不,不同的 BC 设计应该假定它们是完全独立的应用程序。在选项 1 中,Reservations BC 将完成预订,因此在订单失败时补偿“UnReserveProduct”。上述所有情况下的 infra commit 仅适用于 Orders BC。对于选项 2,订单已添加到 repo,但在 Reservations BC 获得肯定结果之前不会提交。
    • 但在这种情况下,您的命令正在修改两个聚合。如果发送 ReserveProduct 后服务器崩溃怎么办?这就是为什么您不在单个命令中更改多个聚合的原因
    • 您在处理程序中所做的是一个流程管理器,您不希望它在请求处理期间,因为一个流程是一系列事件(保留在另一个域上,导致另一种反应),您希望最终完成(也处理失败),但如果您在处理程序中执行此操作,则无法保证最终部分发生错误
    • 谢谢,@rascio。感谢反馈。我渴望了解更多。因此,客户将整个事情视为一个步骤,因此将向 Order BC 发出一个请求。在我看来,这仍然是一个事实,因为不希望客户必须管理流程的细节。所以,我有问题:1)该请求应该由“Saga”而不是直接由 CommandHandler 处理? 2)然后saga可以保持当前的进程状态,然后在崩溃重启后,可以从中断的地方继续吗? 3) Reservations BC 需要幂等函数?
    【解决方案3】:

    一个命令应该更新单个聚合,否则你将打破“聚合”合同,只要这是真的,你可以在你的处理程序中执行你想要的读取次数。

    对于这种情况,事件是最一致的解决方案,但您要付出复杂性的代价才能以这种方式编写软件。

    您应该使用存储库还是服务,这取决于您正在读取的数据是属于处理程序(存储库)的同一有界上下文的一部分还是在不同的上下文(服务)中。

    引入 ReserveProduct 命令是您定义域行为,并且是关于如何在技术上做事的不同问题,您可能想要或不这样做,但这取决于域。

    【讨论】:

      猜你喜欢
      • 2021-05-12
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2012-02-15
      • 2021-06-22
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多