【问题标题】:CQRS read model projection - business logicCQRS读取模型投影-业务逻辑
【发布时间】:2017-06-27 12:01:12
【问题描述】:

因此,我在聚合根目录上触发命令,并且由于该命令而发生了大约 10 个事件。这些事件是内部事件,由于外部系统需要聚合这些事件,我决定进行投影(基本上是阅读投影)。为了从 10 个事件(内部)到 1 个事件(外部)进行此预测,我必须应用一些业务规则(有关事件合并的业务规则)。我应该把这些规则放在哪里,因为它看起来像是域的一部分,但我正在创建内部事件的预测?

基本上由于投影逻辑是域的一部分,我应该将它保存在聚合中并在进行投影的代码中调用它吗?

更新

所以,在一个聚合根中,我有例如3 个事件(内部)作为对一个命令 (aggregate.createPaintandwashatsametime(id, red)) 的响应,该命令发送到聚合根并通过所有聚合根实体传播,例如:CarCreated(Id)、CarSeatColored(Red)、CarWashed( ) 等(所有这 3 个事件都是由于单个命令而发生的)。外部系统期望接收一个外部事件 CarMaintainenceDone(Id, repainted=true,washed=true, somevalue=22);

现在,如果我有一些复杂的逻辑来制作这个 CarMaintainenceDone 事件(比如 if(color==red then in projection somevalue==22 else 44) - 这应该进入投影代码还是域的一部分?

更新 2

让我试着给你一个新的例子。忽略域的建模方式,因为这只是示例:

如您所见,我们有包含 Multiplier 的 AggregateRoot,它只是用来以正确的名称调用事物。当我们进行乘法运算时,我们首先将整数 1 发送到 ObjectA,它有一些逻辑来设置内部状态并发出 ObjectAHasSetParam 事件。 ObjectB 也是如此。最后,ObjectC 监听所有这些事件,并在 paramsHasBeenSet 上进行实际的乘法运算。

在这种情况下,我会在事件存储中保留事件列表:

[ObjectAHasSetParam , ObjectBHasSetParam , ObjectCHasMultiplied ]

我的意思是:如果我将所有这些事件一个一个地发出进程外 - 其他人更新的状态可能会不一致,因为这 3 个事件只有一起才有意义。这就是为什么我想做一些类似投影的东西,但我认为在这种情况下我只需要一起发布这些事件的列表,而不是一个事件一个事件。

class AggregateRoot{
    Multiplier ml;

    void handle(MultiplyCommand(1,2)){
        ml.multiply(1,2);
    }
}

class Multiplier{
    ObjectA a;
    ObjectB b;
    ObjectC res;

    void multiply(1,2){
        a.setParam(1);
        b.setParam(2);
        publish(paramsHaveBeenSet());
    }
}

class ObjectA{
    int p;

    void setParam(1){
        p = 1 + 11;
        publish(ObjectAHasSetParam(12));
    }
}

class ObjectB{
    int p;

    void setParam(2){
        p = 2 + 22;
        publish(ObjectBHasSetParam(24));
    }
}

class ObjectC{
    int p1; int p2;
    int res;

    listen(ObjectAHasSetParam e1){
        p1 = e1.par;
    }

    listen(ObjectBHasSetParam e2){
        p2 = e2.par;
    }

    listen(paramsHaveBeenSet e3){
        res = p1 * p2;
        publish(ObjectCHasMultiplied(288));
    }
}

【问题讨论】:

  • 这是一个奇怪的问题;您应该提供有关上下文的更多信息;事件是什么,你正在合并什么,等等。
  • 什么是“外部域事件”?!
  • CarMaintainenceDone 事件引发后会发生什么? Car Aggregate 是否使用此事件来修改其行为以应对其接收到的未来命令?
  • 没有。这个事件 (CarMaintainenceDone) 被另一个聚合在另一个有界上下文中使用(实际上 saga 接收它并转换为这个另一个聚合的命令)来处理其他一些东西。
  • 查看我回答的最后一句话:“在这个特定的上下文中,我将从 Car 聚合生成事件,以简化代码。”

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


【解决方案1】:

外部系统期望接收一个外部事件 CarMaintainenceDone(Id, repainted=true,washed=true, somevalue=22);

啊哈!简短的回答是流程管理器。

更长的答案是你(应该)现在有两个聚合。其中之一是跟踪汽车的状态。另一个是跟踪保养汽车的过程

暗示在某处隐藏了另一个聚合:你有这个 CarMaintenanceDone 事件,没有聚合负责生成它。所有事件在某处都有一个“聚合”产生它们。聚合可能是现实世界,也可能是现实世界的代理 (HttpRequestReceived),或者是其他有界上下文中的数字事物;但事件告诉你某事在某处改变了状态。

也就是说,您有一些知道何时完成维护的规则的聚合。它是一个信息资源,一个工作日志。当CarWashed 发布时(由汽车、洗衣机或其他),订阅CarWashed 事件的事件处理程序向维护聚合发送一个命令以通知它。维护聚合更新它自己的状态,运行它的逻辑,并在所有单个步骤都被考虑在内时发布一个MaintenanceCompleted 事件。

大多数类似流程的东西都可以实现为聚合;奇怪的是“命令”往往看起来像事件处理程序。但他们有自己的历史(基于他们所观察到的),它描述了状态机如何响应每个观察到的事件而发生变化。

可能不止两个,具体取决于流程的复杂性。

Rinat Abdullin 写了一个很好的introduction to process managers,我经常引用它。

聚合和流程管理器之间没有明显的区别吗?我认为流程管理器只会协调和生活在应用程序服务世界中,根据收到的事件向聚合发送适当的命令。

据我所见——不,没有。文献并没有说得很清楚。

例如,Udi Dahan wrote

这是我能告诉你的最有力的迹象,表明你正在正确地执行 CQRS:你的总根是 sagas。

Saga,在这里,相当于一个进程。

【讨论】:

  • 这是一个有趣的想法。聚合和流程管理器之间没有明确的区别吗?我认为流程管理器只会协调和生活在应用程序服务世界中,根据收到的事件向聚合发送适当的命令。
  • 我会在这里看到更多调用聚合的事件处理程序(例如CarWashed 处理程序)作为流程管理器,不是吗?
  • 我计划在我的博客上写一篇更长的文章;发布时我会在此处链接。
  • 太好了,谢谢!如果聚合根是传奇,尽管它们必须耦合到一些基础设施问题才能成为事件处理程序本身,不是吗?除非两者之间有中介,我一直认为这是流程管理器位。
【解决方案2】:

通常有 2 种事件模型,内部事件(仅在 BC 中可见)和外部事件(发布到外部世界)。您可以决定将所有内容都外部化,但随后您必须对所有内容进行版本控制。

您可以在Patterns, Principles, and Practices of Domain-Driven Design book p.408 中阅读有关内部与外部事件的更多信息(在链接中向上滚动一点)。

Projections 不应该负责发布外部事件。一种常见的做法是从应用程序服务层注册一个内部事件处理程序,该处理程序负责在消息传递基础架构上发布外部事件。您可以利用该过程将这些事件聚合在一起并从中发布单个外部事件。

如何执行聚合取决于您,但由于内部事件可以同步引发并且处理程序通常是单线程的,您只需在处理程序中设置一个状态机,当它接收到第一个事件时启动批处理并聚合它们,直到收到最后一个,然后在消息总线上发布。

如果您的消息传递基础架构无法与您的事件存储参与相同的事务,您可能只需要一个额外的进程来按顺序读取已提交的事件并执行与上述相同的操作。

另一种方法是让消费者处理聚合。如果消费者能够否决“CarMaintenanceDone”的含义,这可能是正确的做法。

最后,您还可以从聚合本身发布一个额外的事件。 AR 本身可能不会利用该事件,但有时最好只做更实用的事情(就像使用仅由读取模型使用的数据来丰富事件一样)。如果添加更多事件,这种方法还具有无需更改逻辑的优点。

【讨论】:

  • 是的。更不了解这一切。只是如果我使用你在上一部分中提到的这个外部过程——我实际上为外部系统创建了事件到这个新事件中的投影。似乎您还必须将部分业务逻辑放入 CQRS 的 Q 部分的生成中。
  • “事件聚合的事件发布”如何用于投影重建?我认为这种事件聚合的正确方法是使用创建命令的 Saga
  • @bojanv55 我不会称之为投影。它是一个事件处理器,可将内部事件流转换为外部事件流。它的目标与投影不同,不会被视为 Q 部分。
  • @ConstantinGALBENU 我不确定这与预测有什么关系,甚至更少重建。一切都发生在命令处理世界中。
  • 我不明白这个生成额外事件的“内部事件处理程序”是什么东西。
【解决方案3】:

不应该有外部事件的概念。事件由聚合生成并由同步读取模型、saga 使用或发布到外部世界,其他系统和微服务可以随意使用它们。

因此,在您的情况下,消费者(例如实现为 saga)应该通过其业务规则聚合这些事件,然后做一些事情(例如,saga 可以创建一个新命令)而不是聚合。

更新(针对正在更新的问题)

如果您认为汽车维护是 Car 聚合的责任,那么 Car 聚合应该引发该事件。这取决于Car 聚合的未来行为如何受CarMaintainenceDone 事件的影响。在这个特定的上下文中,我将从 Car 聚合生成事件,以简化代码。

【讨论】:

  • 只是 CarMaintainenceDone 实际上是这 3 个事件的投影。所以你基本上是说我也应该从同一个聚合(汽车)中提出这个 CarMaintainenceDone 事件,即使它只是这 3 个事件的投影?由于我也在使用事件溯源,因此这 3 个事件将被存储到事件存储中。在这种情况下,CarMaintainenceDone 只是一个应该向外部消费者广播的事件。
  • “不应该有外部事件的概念”这是不正确的。看我的回答。
  • @plalx 从我的角度来看,所有事件都被持久化到事件存储中。然后,这些事件被发布到外部世界给任何需要它们的消费者。您对事件聚合器的想法很有趣。这些“聚合事件”是否保留在事件存储中?
猜你喜欢
  • 2021-03-25
  • 1970-01-01
  • 1970-01-01
  • 2012-04-25
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多