关于CQRS,有一件事我没有得到:当引发事件不包含更新读取模型所需的详细信息时,如何更新读取模型。
不幸的是,这是一种非常常见的情况。
示例:我将用户添加到组中,因此我发送addUserToGroup(userId,groupId)命令。收到此信息,由命令处理程序处理,创建,存储和发布userAddedToGroup事件。
现在,事件处理程序接收此事件和两个ID。现在应该有一个视图,列出所有用户都有他们所在的组的名称。要更新该视图的读取模型,我们需要用户ID(我们拥有)和组名(我们不知道)我有,我们只有它的身份。
所以问题是:我该如何处理这种情况?
目前,我想到了四种选择,都有其特殊的缺点:
读取模型询问域。 =>禁止,甚至不可能,因为域只有行为,没有(公共)状态。
读取模型从读取模型中的另一个表中读取组名。 =>工作,但如果没有匹配的表怎么办?
将必要的数据添加到事件中。 =>不起作用,因为这意味着我必须更新所有以前的事件,我无法预见有一天我可能需要哪些数据。
不要通过“常规”事件处理程序处理事件,而是在后台启动一个处理事件存储的ETL进程,创建必要的数据并写入读取的模型。 =>可行,但对我而言,对于这样一个简单的场景,这似乎有点太多开销。
所以,问题是:我如何正确处理这种情况?
答案 0 :(得分:7)
有两种常见的解决方案。
1)“事件充实”是您确实将信息放在反映您提及的信息的事件上,例如组名。这样做可以在正确建模域名和欺骗之间进行。例如,如果您知道组名称发生更改,则在更改时发出名称并不是一个坏主意。想象一下,当您在报价或发票上创建订单项时,您希望在发票创建的事件中排出所售商品的价格。这是因为你必须尊重这个价格,即使它稍后改变。
2)一次投射几个流。编写一台投影仪,观察各种流中的信息并将它们连接在一起。您可以观看用户和群组事件以及添加到群组活动的用户。根据系统中事件的顺序,您可能知道用户在知道组名称之前就在组中,但在开始之前您应该知道事件存储的常规属性。
答案 1 :(得分:5)
事件不一定代表首先启动过程的命令的一对一映射。例如,如果您有一个命令:
SubmitPurchaseOrder
Shopping Cart Id
Shipping Address
Billing Address
结果事件可能如下所示:
PurchaseOrderSubmitted
Items (Id, Name, Amount, Price)
Shipping Address
Shipping Provider
Our Shipping Cost
Shipping Cost billed to Customer
Billing Address
VAT %
VAT Amount
First Time Customer
...
通常信息可用于域模型(通过命令提供或作为相关聚合的已知内部状态或作为处理的一部分计算。)
此外,可以通过在处理过程中查询读取模型甚至不同的BC(例如,根据状态检索实际增值税%)来丰富事件。
您正确地假设事件可以(并且可能会)随时间而变化。如果您使用版本控制,这基本上无关紧要:添加新事件(例如SubmitPurchaseOrderV2
)并向应该使用它的所有类添加适当的事件处理程序。无需更改旧事件,因为您不修改界面,所以仍然可以使用旧事件。这基本上归结为实践中开放/封闭原则的一个很好的例子。
答案 2 :(得分:0)
选项2没问题,关于“组名称读取模型表中的不匹配怎么样”的问题不适用。不应删除任何数据,如果以前的事件(例如删除组)被删除则应该无效。最后,groups表中的行有效,您可以毫无问题地读取组名。唯一明显的问题可能是速度不一致,但这是另一个问题,无论处理速度如何,事件都应该有序处理。