加载中

SOME SAY: “CQRS IS HARD!”
IS IT? WELL, I USED TO THINK THE SAME! BUT WHEN I STARTED WRITING MY FIRST PIECE OF SOFTWARE USING CQRS, IT QUICKLY TURNED OUT THAT IT IS RATHER UNCOMPLICATED. WHAT IS MORE, I THINK THAT IN LONG TERM IT IS MUCH EASIER TO MAINTAIN SOFTWARE WRITTEN IN THIS WAY.

I have started to think, what is the reason that people see it as hard and complex at the beginning? I have a theory: it has rules! Entering the world with rules is always uncomfortable, we need to adjust to rules. In this post I’d like to prove that in this case those rules are quite digestible.

有人说:“CQRS很难!”

是吗? 好吧,我也曾这样认为! 但,当我开始使用 CQRS 编写我的第一个软件时,它很快就不攻自破。更为重要的是,我认为从长远来看,以这种方式维护软件更加容易。

我开始思考:为何人们在一开始时认为它是多么困难难和复杂? 我有一个理论:它包含规则! 进入拥有规则的世界总是不舒服的,我们需要适应这些规则。在这篇文章中,我想证明在这种情况下,这些规则是非常易于理解的。

ON THE WAY TO CQRS…

Basically, we can say that CQRS is an implementation of Command Query Separation principle to the architecture of software. What I noticed during my work in this approach is that there is a couple of steps between simplest CQS implementation and real full-blown CQRS. I think that those steps smoothly introduce rules I have mentioned before.

While the first steps are not fulfilling my definition of CQRS (but are sometimes so called), they can still introduce some real value to your software. Each step introduces some interesting ideas that may help to structure or clean your codebase/architecture.

Usually, our journey starts with something that looks like this:

在通往 CQRS 的路上…

从根本上来说,我们可以将 CQRS 视为对软件架构命令查询分离规则的实现。在使用此方法的工作中,我注意到在最简单的 CQS 实现与真正成熟的 CQRS 之间有几个步骤。我想那些步骤可以顺利地引入我之前已经提到的规则。

虽然第一步没有实现我对 CQRS 的定义(但是有时候这么称呼的),但是他们还可以为你的软件引入一些真正的价值。每个步骤都引入一些有趣的想法,可以有助于构建或清理你的代码库/架构。

通常,我们的旅程从这里开始:

This is typical N-Layer architecture as all of us probably know. If we want to add some CQS here, we can “simply” separate business logic into Commands and Queries:

If you are working with legacy codebase this is probably the hardest step as separating side-effects from reads in spaghetti code is not easy. In the same time this step is probably the most beneficial one; it gives you an overview where your side effects are performed.

Hold for a second! You are talking about CQS, CQRS, but you have not defined what Command or Query is!

That is true. Let’s define them now! I’ll give you my personal, intuitive definition of Command and Query here. It is not exhaustive and definitely should be deepened before implementation.

我们可能都知道,这是一个典型的 N-层架构。如果我们想在这添加一些 CQS,我们可以“简单地”将业务逻辑层分离为命令和查询:

如果你还在使用老式代码库,这可能是最难的一步,就像从意大利面式代码中阅读分离出副作用一样不简单。同时这个步骤可能也是最有好处的一个;它会给你一个副作用执行的位置的概述。

等一下!你正在讨论 CQS,CQRS ,但是你还没有定义到底什么是命令或查询!

没错。我们开始定义它们吧!在这里,我会给你我个人、直观的对命令和查询的定义。它并不全面,而且在实现之前必须加以深化。

Command – First of all,firing command is the only way to change the state of our system. Commands are responsible for introducing all changes to the system. If there was no command, the state of the system remains unchanged! Command should not return any value. I implement it as a pair of classes: Command and CommandHandler. Command is just a plain object that is used by CommandHandler as input value (parameters) for some operation it represents. In my vision command is simply invoking particular operations in Domain Model (not necessarily one operation per command).

Query – Analogically, query is a READ operation. It reads the state of the system, filters, aggregates and transforms data to deliver it in the most useful format. It can be executed multiple times and will not affect the state of the system. I used to implement them as one class with some Execute (…) method, but now I think that separation to Query and QueryHandler/QueryExecutor may be useful.

命令——首先,触发命令是唯一改变系统状态的方法。命令负责引起所有的对系统的改变。如果没有命令,系统状态保持不变!命令不应该返回任何值。我使用两个类来实现它:Command 和 CommandHandler 。Command 只是一个普通的对象,CommandHandler 将它用于表示某些操作的输入值(参数)。我认为命令是简单地调用领域模型中的特定操作(不一定是每个命令都有的操作)。

查询——同样的,查询是一个读操作。它读取系统的状态,过滤,聚总,以及转换数据,并将其转化为最有用的格式。它可以执行多次,而且不会影响系统的状态。我之前是使用一个有一些 Execute(…) 函数的类来实现它,但是现在我认为分离成 Query 和 QueryHandler/QueryExecutor 可能会更有用。

Going back to the diagram, I need to clarify one more thing; I’ve smuggled here one additional change, Model became the Domain Model. By the Model I understand a group of containers for data while Domain Model encapsulates essential complexity of business rules. This change is not directly affecting our further considerations as we are interested in architectural stuff here. But it is worth mentioning that despite the fact that the command is responsible for changing state of our system, the essential complexity should be placed in the Domain Model.

OK, now we can add new command or write new query. After short period of time it will be quite obvious that Domain Model that works well for writing is not necessarily perfect for reading. It is not a huge discovery that it would be easier to read data from some specialized model:

回到示意图,我需要澄清一些事情;我已经隐秘地做了一个补充修改,模型改为领域模型。由于我认为模型是一组数据容器,而领域模型包括了业务规则中本质复杂性。因为我们对这里的体系架构感兴趣,这个修改不会直接影响我们的进一步考虑。但是值得一提的是,尽管命令负责改变系统的状态,本质复杂性应该放到领域模型。

好的,现在我们可以添加新的命令或者编写新的查询。短时间内,很明显,适用于写的领域模型并不一定适合读。从某种特殊模型中更容易读取数据,这并不是一个重大的发现:

We can introduce separated model, mapped by ORM and use it to build our queries, but in some scenarios, especially when ORM introduces overhead, it would be useful to simplify this architecture:

I think that this particular change should be well thought out!

Now the problem is that we still have READ and WRITE model separated only at the logical level as both of them shares common database. That means we have separated READ model but most likely it is virtualized by some DB Views, in better case Materialized Views. This solution is OK if our system is not suffering from performance issues and we remember to update our queries along with WRITE model changes.

我们可以引入分离模型,由 ORM 映射并构建查询,但是在某些情况下,特别是当 ORM 引入开销时,它将对简化结构有所帮助。

我认为这个特殊的改变应当被好好地考虑!

现在的问题是我们仍然有仅在逻辑层级上分离的读和写模型,因为他们共享公共数据库。这就意味着我们已经分离了读模型,但最有可能是被一些 DB 视图给虚拟化了,物化视图的情况下更好。如果我们的系统没有性能问题,并且我们记住在写模型改变的时候更新查询,那这个方案是可行的。

The next step is to introduce fully separated data models:


From my point of view this is the first model that fulfills original idea presented by Greg Young, now we can call it CQRS. But there is also a catch! I’ll write about it later.

下一步是引入完全分离的数据模型:

在我看来,这是第一个符合 Greg Young 提出的原始想法的模型,现在我们称它为 CQRS 。但是它仍然有问题!我之后再写。

CQRS != EVENT SOURCING

Event Sourcing is an idea that was presented along with CQRS, and is often identified as a part of CQRS. The idea of ES is simple: our domain is producing events that represent every change made in system. If we take every event from the beginning of the system and replay them on initial state, we will get to the current state of the system. It works similarly to transactions on our bank accounts; we can start with empty account, replay every single transaction and (hopefully) get the current balance. So, if we have stored all events, we can always get the current state of the system.

While ES is a great method to store the state of the system is not necessarily required in CQRS. For CQRS, it is not important how Domain Model is actually stored and this is just one of options.

CQRS != 事件溯源

事件溯源是与 CQRS 一起提出的一个概念,通常被标识为 CQRS 的一部分。ES(Event Sourcing)的概念很简单:我们的领域生成的事件表示系统中的每一个更改。如果我们从系统开始记录每一个事件,而且从最初状态开始重现,我们会得到系统的当前状态。它与银行账户的事务相似;我们可以从空账户开始,重现每一个单独的事务,然后(有希望地)得到当前的余款。因此,如果我们已经存储了所有的事件,我们能得到系统的当前状态。

虽然 ES 是存储系统的状态的一种很好的方法,但是 CQRS 并不一定需要它。对于 CQRS ,领域模型实际上如何存储并不重要,而且这只是一个选项。

READ AND WRITE MODEL

The idea of separated models seems to be quite clear and straight-forward when we are reading about CQRS but it seems to be unclear during implementation. What is the responsibility of WRITE model? Should I put all data into my READ model? Well, it depends!

WRITE MODEL

I like to think about my WRITE model as about the heart of the system. This is my domain model, it makes business decisions, it is important. The fact that it makes business decisions is crucial here, because it defines major responsibility of this model: it represents the true state of the system, the state that can be used to make valuable decisions. This model is the only source of truth.

If you want to know more about designing domain models I recommend reading about Domain Driven Design technique philosophy.

读模型和写模型

当我们阅读 CQRS 时,分离模型的概念似乎非常清晰和直接,但在实现过程中似乎并不清楚。写模型的责任是什么?我是否应该将所有数据放入我的读取模型中?嗯,这得看情况!

写模型

我喜欢把我的写作模型看作是系统的核心。这是我的领域模型,它做业务决策,它很重要。它做出业务决策的事实在这里是至关重要的,因为它定义了这个模型的主要职责:它代表系统的真实状态,可以用来做出有价值的决策的状态。这种模式是唯一的真理来源。

如果你想了解更多关于设计领域模型的知识,我推荐你阅读领域驱动设计技术哲学。

READ MODEL

In my first attempt to CQRS I have used WRITE model to build queries and … it was OK (or at least worked). After some time we reach the point in the project where some of our queries were taking a lot of time. Why? Because we are programmers and optimization is our second nature. We designed our model to be normalized, so our READ side were suffering from JOINs. We were forced to pre calculate some data for reports to keep it fast. That was an quite interesting, because in fact we have introduced a cache. And from my point of view this is the best definition of READ model: it is a legitimate cache. Cache that is there by design, and is not introduced because we have to release project and non-functional requirements are not met.

The label READ model can suggest that it is stored in a single database and that’s it. In fact READ model can be very complex, you can use graph database to store social connections and RDBMS to store financial data. This is the place where polyglot persistence is natural.

读模型

在我第一次尝试 CQRS 时,我使用了 WRITE 模型来构建查询……它是 OK 的(或者至少是有效的)。过了一段时间,我们到达了项目中需要花费大量时间进行查询的地方。为什么?因为我们是程序员,优化是我们的第二天性。我们将模型设计为规范化,因此我们的读取端受到连接的影响。我们被迫预先计算一些报告的数据以保持快速。这很有趣,因为实际上我们引入了缓存。在我看来,这是读取模型的最佳定义:它是一个合法的缓存。由于我们必须发布项目,而非功能性的需求没有得到满足,因此,缓存是通过设计来实现的。

标签读取模型可以建议它存储在一个数据库中,仅此而已。实际上读取模型可能非常复杂,你可以使用图形数据库来存储社会连接,使用 RDBMS 来存储财务数据。这是一个多语言持久性很自然的地方。

返回顶部
顶部