【问题标题】:How do you handle command side-affects in a CQRS architecture?您如何处理 CQRS 架构中的命令副作用?
【发布时间】:2025-11-23 02:15:02
【问题描述】:

我们开始在我们的系统中发现一些场景,其中针对聚合的命令结果可能会影响其他相关聚合。

为了演示这个问题,考虑一个树结构,其中我们的节点有兄弟姐妹。每个节点都有一个排名,以确定它们在 UI 中的显示顺序,即

Node 1 | Ranking = 10
Node 2 | Ranking = 11
Node 3 | Ranking = 12
Node 4 | Ranking = 13
Node 5 | Ranking = 14

我们的节点聚合有一个不变量,它规定不能将排名设置为低于特定值(我们称之为 10)。如果将排名设置为低于此值,则会导致重新计算 所有 兄弟节点(包括相关节点)。为简单起见,假设计算只是根据上一个兄弟姐妹排名的两倍来计算排名

Node 1 | Ranking = 9 (cannot be accepted, reset to 50)
Node 2 | Ranking = 100
Node 3 | Ranking = 200
Node 4 | Ranking = 400
Node 5 | Ranking = 800

关键是,针对一个聚合的命令会导致更改到另一个聚合(或在这种情况下为多个)。

目前采取的方法是在到达域的途中拦截这些命令,“修复”它们并然后发送它们。所以在上面的示例场景中

  • 我们收到来自客户的ChangeNodeRankingCommand
  • 我们验证命令,即它是否在 10 以上的新排名
  • 如果命令有效,则将其发送到域
  • 如果命令无效,我们查询读取端提取所有受影响的聚合ID
  • 我们使用调整后的排名为每个聚合(包括有问题的聚合)创建一个命令
  • 我们将这些命令发送到域并丢弃原始命令

这很好用,但是有一些问题

  • 我们一直在处理可能导致问题的陈旧数据(尝试更新随后被删除的实体等)
  • 这些东西似乎是域逻辑,因此应该由域来处理

选择这样做的原因纯粹是从查询的角度来看,我们在我们的域中使用Event Sourcing,因此加载相关的聚合并不是微不足道的(考虑到我们事件存储的性质)。

这是一种合理的方法还是我在这里完全错过了一个技巧?

【问题讨论】:

    标签: domain-driven-design distributed cqrs


    【解决方案1】:

    乍一看(考虑到节点/排名的东西很可能被简化了),您的“排名”属性的聚合边界似乎放错了位置,特别是如果一个节点的排名影响其排名兄弟姐妹,这显然跨越了 AR 的界限。

    在典型的树结构中(在数学意义上),所有兄弟姐妹的父节点可能被认为负责其子节点的排序。在此模型中,如果您随后向这些孩子的父母发送ChangeNodeRankingCommand,则所有孩子的重新排名都会在 AR 上进行。

    如果“排名”有更复杂的含义,您可以尝试将父节点中的排序与单个节点的“排名”属性与 saga 分离(即,每当排名变化)。

    【讨论】:

    • 我预感到这种方法不正确(因此提出了问题)。因此,不要为每个节点聚合使用单独的命令,而是向父节点发送一个 single 命令,例如CalculateChildRankingsCommand 并在域中执行此操作?这确实有道理,唯一的缺点是我当前的设置不允许在写入端进行这种类型的查询。实际上,我需要加载 every 节点,然后按父节点过滤 - 我想这更像是一个设计问题。
    • 是的,我就是这个意思。至于查询问题,当然也可以使用投影。
    • 当你说我可以使用投影时,你的意思是让写端查询读端?或者实际上在写入端创建了预测?我是整个 CQRS 游戏的新手,我发现最大的问题是这种类型的东西没有任何明确定义的规则。目前我的域只能单独加载一个聚合(它不能加载相关的聚合)。
    • 如果您的父节点需要拥有其所有子节点的列表或一些信息来执行其操作,例如,您可以向您的域层添加一个域服务接口,让父节点查询必要的信息。然后可以通过读取模型(由投影支持)来实现该领域层(在领域层之外,就像存储库一样),这样查询速度很快,并且不需要涉及查询。
    • 所以这个域服务会查询读取端?这不是我之前谈到的同一个问题,通过查询读取端意味着您(可能)正在处理陈旧的数据吗? “读取模型(由投影支持)”我现在有点感到困惑,读取模型/投影不是一回事吗?
    最近更新 更多