没有DDD和没有(或带有?)ES的CQRS-什么是写模型,什么是读模型?


11

据我了解,CQRS背后的主要思想是拥有2个不同的数据模型来处理命令和查询。这些被称为“写入模型”和“读取模型”。

让我们考虑一个Twitter应用程序克隆的示例。以下是命令:

  • 用户可以自己注册。CreateUserCommand(string username)发出UserCreatedEvent
  • 用户可以关注其他用户。FollowUserCommand(int userAId, int userBId)发出UserFollowedEvent
  • 用户可以创建帖子。CreatePostCommand(int userId, string text)发出PostCreatedEvent

虽然我在上面使用了“事件”一词,但我并不是说“事件源”事件。我的意思是触发读取模型更新的信号。我没有活动存储,到目前为止,我想专注于CQRS本身。

这是查询:

  • 用户需要查看其帖子列表。 GetPostsQuery(int userId)
  • 用户需要查看其关注者列表。 GetFollowersQuery(int userId)
  • 用户需要查看其后的用户列表。 GetFollowedUsersQuery(int userId)
  • 用户需要查看“朋友供稿”-他们所有朋友活动的日志(“您的朋友约翰刚刚创建了一个新帖子”)。 GetFriedFeedRecordsQuery(int userId)

要处理,CreateUserCommand我需要知道这样的用户是否已经存在。因此,在这一点上,我知道我的写模型应该具有所有用户的列表。

要处理,FollowUserCommand我需要知道userA是否已经跟随userB。在这一点上,我希望我的写模型具有所有用户关注用户连接的列表。

最后,要处理CreatePostCommand我不需要其他任何东西,因为我没有诸如之类的命令UpdatePostCommand。如果有这些内容,则需要确保该帖子存在,因此我需要所有帖子的列表。但是因为我没有此要求,所以不需要跟踪所有帖子。

问题1:以我使用的方式使用术语“写模型”实际上正确吗?还是ES的“写模型”总是代表“事件存储”?如果是这样,我需要处理命令的数据和我需要处理查询的数据之间是否存在任何分隔?

要处理GetPostsQuery,我需要所有帖子的列表。这意味着我的阅读模型应具有所有帖子的列表。我将通过收听来维护此模型PostCreatedEvent

要同时处理GetFollowersQueryGetFollowedUsersQuery,我需要一个用户之间所有连接的列表。为了维持这种模式,我要听UserFollowedEvent。这是第2问题:如果我在这里使用写模型的连接列表,实际上可以吗?还是我应该更好地创建一个单独的读取模型,因为将来可能需要比写入模型更多的细节?

最后,要处理,GetFriendFeedRecordsQuery我需要:

  • UserFollowedEvent
  • PostCreatedEvent
  • 了解哪些用户关注其他用户

如果用户A跟随用户B并且用户B开始跟随用户C,则应显示以下记录:

  • 对于用户A:“您的朋友用户B刚刚开始关注用户C”
  • 对于用户B:“您刚刚开始关注用户C”
  • 对于用户C:“用户B现在正在关注您”

这里的问题3:我应该用什么模式来获取连接列表?我应该使用写模型吗?我应该使用读取模型- GetFollowersQuery/ GetFollowedUsersQuery吗?还是应该让GetFriendFeedRecordsQuery模型本身处理UserFollowedEvent并维护自己的所有连接列表?


请记住,在CQRS系统中,查询数据模型和命令数据模型可能正在使用来自不同数据库的数据。两种模式可能彼此独立地生活(不同的应用程序)。话虽如此,答案是“取决于”(照常)。可能会感兴趣
Laiv

Answers:


7

格雷格·杨(2010)

CQRS只是在以前只有一个的两个对象的创建。

如果您以Bertrand Meyer的Command Query Separation来考虑,您可以认为该模型具有两个不同的接口,一个支持命令,另一个支持查询。

interface IChangeTheModel {
    void createUser(string username)
    void followUser(int userAId, int userBId)
    void createPost(int userId, string text)
}

interface IDontChangeTheModel {
    Iterable<Post> getPosts(int userId)
    Iterable<Follower> getFollowers(int userId)
    Iterable<FollowedUser> getFollowedUsers(int userId)
}

class TheModel implements IChangeTheModel, IDontChangeTheModel {
    // ...
}

格雷格·杨(Greg Young)的见解是,您可以将其分为两个单独的对象

class WriteModel implements IChangeTheModel { ... }
class ReadModel  implements IDontChangeTheModel {...}

分离了对象之后,您现在可以选择分离将对象状态保存在内存中的数据结构,以便针对每种情况进行优化。或将读状态与写状态分开存储/保存。

问题1:以我使用的方式使用术语“写模型”实际上正确吗?还是ES的“写模型”总是代表“事件存储”?如果是这样,我需要处理命令的数据和我需要处理查询的数据之间是否存在任何分隔?

通常将术语WriteModel理解为模型的可变表示(即:对象,而不是持久性存储)。

TL; DR:我认为您的用法很好。

这是一个问题2:如果我在这里使用写模型的连接列表,实际上可以吗?

很好。从概念上讲,共享相同结构的读取模型和写入模型没有任何问题。

实际上,由于对模型的写入通常不是原子的,因此当一个线程试图修改模型的状态而另一个线程试图读取模型的状态时,则可能存在问题。

问题#3:应该使用哪种模型来获取连接列表?我应该使用写模型吗?我应该使用读取模型-GetFollowersQuery / GetFollowedUsersQuery吗?还是应该让GetFriendFeedRecordsQuery模型本身处理UserFollowedEvent并维护其自己的所有连接列表?

使用写模型是错误的答案。

编写多个读取模型(将每个模型都调整到特定的用例)是完全合理的。我们说的是“读取模型”,但可以理解的是,可能有许多读取模型,每个读取模型都针对特定的用例进行了优化,并且没有实现没有意义的情况。

例如,您可能决定使用键值存储来支持某些查询,并使用图数据库进行其他查询,或者使用该查询模型有意义的关系数据库。马匹的课程。

在您仍在学习模式的特定情况下,我的建议是使设计保持“简单”-拥有一个不共享写模型数据结构的读模型。


1

我鼓励您考虑拥有一个概念性数据模型。

然后,写模型是该数据模型的实现,该模型针对事务更新进行了优化。有时这意味着标准化的关系数据库。

读取模型是为执行您的应用程序所需要的查询而优化的同一数据模型的实例化。尽管有意地对其进行了规范化以处理具有较少联接的查询,但它仍可能是关系数据库。


(#1)对于CQR​​S,写模型不必是事件存储。

(#2)我不希望读取模型存储任何不在写入模型中的内容,因为在CQRS中,没有人更新读取模型,只有转发机制使读取模型与对更改的同步保持同步。写模型。

(#3)在CQRS中,查询应与读取模型相反。您可以做不同的事情:没关系,只是不遵循CQRS。


总之,只有一个概念数据模型。CQRS将命令网络和功能与查询网络和功能分开。鉴于这种分离,可以使用截然不同的技术作为性能优化来托管写模型和读模型。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.