存储库如何适应 CQRS?

Posted

技术标签:

【中文标题】存储库如何适应 CQRS?【英文标题】:How do Repositories fit with CQRS? 【发布时间】:2012-04-20 17:26:57 【问题描述】:

根据 Fowler (here) 的说法,存储库“在域和数据映射层之间进行调解,就像内存中的域对象集合一样”。因此,例如,在我的 Courier Service 应用程序中,当提交新的运行时,我的应用程序服务会创建一个新的运行聚合根对象,使用请求中的值填充它,然后在调用工作单元保存之前将其添加到 RunRepository对数据库的更改。当用户想要查看当前运行的列表时,我会查询同一个存储库并返回一个表示信息的非规范化 DTO。

但是,在查看 CQRS 时,查询不会访问同一个存储库。相反,它可能会直接违背数据存储并且总是被非规范化。我的命令端将演变为 NewRunCommand 和 Handler,它们将创建和填充 NewRun 域对象,然后将信息保存到数据存储中。

所以第一个问题是,如果我们不维护域对象的内存中集合(缓存,如果你愿意的话),存储库在哪里适合 CQRS 模型?

考虑这样一种情况,提交给我的应用程序服务的信息只包含一系列 ID 值,服务必须解析这些 ID 值才能构建域对象。例如,请求包含分配给运行的快递员的 ID #。服务必须根据 ID 值查找实际的 Courier 对象,并使用 AssignCourier 方法(该方法验证 courier 并执行其他业务逻辑)将该对象分配给 NewRun。

另一个问题是,考虑到查询的分离和可能没有存储库,应用程序服务如何执行查找以找到 Courier 域对象?

更新

根据丹尼斯评论后的一些额外阅读和思考,我将重新表述我的问题。

在我看来,CQRS 鼓励存储库,这些存储库仅仅是数据访问和数据存储机制的门面。它们给出了集合的“外观”(如 Fowler 所描述的),但不管理内存中的实体(如 Dennis 指出的那样)。这意味着对存储库的每个操作都是直通的,是吗?

工作单元如何适应这种方法?通常,UoW 用于提交对存储库所做的更改(对吗?),但如果存储库不维护内存中的实体,那么 UoW 有什么作用?

关于“写入”操作,命令处理程序是否会引用相同的存储库、不同的存储库或可能是 UoW 而不是存储库?

【问题讨论】:

Fowler 的定义与 Eric Evan 在 DDD 书中描述的存储库模式略有不同。我认为 CQRS 倾向于 Evan 的定义。 请解释一下。我能看到的唯一区别是“内存中”方面,但用法是一样的,对吧? 【参考方案1】:

我不确定这是多么正统 - 但在当前项目中,我有一个用于聚合实体根的存储库。这个存储库只有两个方法,Get 和 ApplyEvents。

所有事件都为其类型实现了一个通用接口——订单有OrderEvents等。我个人将每个事件的业务逻辑都放到了一个多态方法中,这样添加新类型的事件就变得非常容易了。

对于 Get,存储库转到事件存储,并获取该类型范围内的所有事件(例如,单个存储位置订单)。然后,它对事件进行重播,以针对它给出的所有事件到达实体的当前状态。它也可以从快照中工作,因此您不必在每次加载时都重新创建每个事件。您还可以拥有一个通用事件存储库,甚至可以抽象出您存储事件的方式,并根据规范检索它们。

ApplyEvents 接收一个事件列表,然后根据这些改变实体的状态,并返回它。请注意,您为存储库提供了重新创建实体的选项,而不仅仅是更改它!这适用于函数式编程,但意味着最好在 C# 或 Java 中避免对象相等 (obj1 == obj2)。不过,我认为只有 ValueObjects 而不是 Entities 应该永远具有平等性。

这是它在实践中的工作方式(C#)- 我有订单,我想添加一个项目。 currentOrder.Items 返回一个空列表。然后我做

Assert.IsFalse(newEvent.Items.Any())
IOrderEvent newEvent = eventFactory.CreateOrderItemEvent(myItemID);
currentOrder = orderRepository.ApplyEvents(currentOrder, newEvent);
Assert.IsTrue(newEvent.Items.Any())

我现在应该看到 currentOrder.Items 有一个条目。

这里的缺点是我所有的处理都是通过事件完成的,而不是我的业务逻辑在实体中。但是在我的情况下,我的几乎所有对象都需要可序列化(基本上是 POCO)并在多个系统上工作,这实际上效果很好。

【讨论】:

【参考方案2】:

我读过 CQRS 系统,它们在命令端维护一个简单的键值存储来表示应用程序的状态,以及其他仅关联消息(使用某种 saga)并利用查询存储来表示应用程序状态的系统反而。无论哪种方式,这些方法无疑都会涉及持久性技术,但在这些情况下,存储库模式将是对其顶部的不必要抽象。

不过,我在 CQRS 方面的经验只涉及事件溯源,我们重放过去的事件以重建封装并强制执行业务逻辑和不变量的聚合。在这种情况下,存储库模式是一种熟悉的抽象,可以提供一种更简单的方法来检索这些聚合中的任何一个。

关于查询方面,我建议尽可能靠近数据存储,我的意思是避免在您的 UI(无论是什么)和数据存储之间存在任何存储库、服务或外观等。

查看这些方法的使用示例可能会有所帮助。不妨看看以下项目:

https://github.com/gregoryyoung/m-r/tree/master/SimpleCQRS https://github.com/MarkNijhof/Fohjin https://github.com/elliotritchie/NES https://github.com/ToJans/Scritchy http://ncqrs.org

在 NES 的情况下,存储库仅提供一个熟悉的界面,用于直接在工作单元中添加和读取聚合。

更多可能有帮助的链接:

CQRS Examples and Screencasts Event Sourcing Resources

【讨论】:

我同意大多数示例将 CQRS 与事件溯源相结合。但是,我与 Udi Dahan 的心态更加相似,他指出这种关系是在实现选择中而不是 CQRS 模式的一部分。 事件溯源增加了我们将更改作为事务保存而不是保存实体状态的想法。我可以看到它的价值,但考虑到这需要专业知识和思维过程,这是一种很难采取的方法。而且,就我而言,我正在处理许多机构开发人员和遗留应用程序,包括我必须继续使用的遗留数据库。所以我的 CQRS 实现不包括事件溯源。 您可能会发现让您的事件模型和代表您的应用程序状态的模型保持同步变得很困难。 不确定你的意思。正如我所说,我们没有使用事件溯源。 再次尝试回答这个问题 - 我会在我的命令端使用一个非通用存储库来提供一个明确的合同来检索我的域模型对象(FindCustomerById、FindCustomerByName 等)。存储库可以委托给一个工作单元,该工作单元将管理检索到的任何对象的内存集合。在查询方面,我直接从数据存储中读取 - 没有存储库。

以上是关于存储库如何适应 CQRS?的主要内容,如果未能解决你的问题,请参考以下文章

java CQRS系统的基本存储库

NestJS CQRS:(存储库的)依赖注入在处理程序中不起作用

服务层如何适应我的存储库实现?

只读数据库视图如何适应存储库模式?

CQRS不等同于读写分离

如何写出简洁的 CQRS 代码?