千家信息网

如何使用CQRS避免查询对模型设计的影响

发表于:2024-11-27 作者:千家信息网编辑
千家信息网最后更新 2024年11月27日,这篇文章主要为大家展示了"如何使用CQRS避免查询对模型设计的影响",内容简而易懂,条理清晰,希望能够帮助大家解决疑惑,下面让小编带领大家一起研究并学习一下"如何使用CQRS避免查询对模型设计的影响"
千家信息网最后更新 2024年11月27日如何使用CQRS避免查询对模型设计的影响

这篇文章主要为大家展示了"如何使用CQRS避免查询对模型设计的影响",内容简而易懂,条理清晰,希望能够帮助大家解决疑惑,下面让小编带领大家一起研究并学习一下"如何使用CQRS避免查询对模型设计的影响"这篇文章吧。

模型设计时不要考虑查询

按DDD设计领域模型时,有两个很有用的原则:

  • 模型对展现技术要无感知,比如展现是用WEB,还是APP端,在做模型设计时不要考虑

  • 不要关心复杂查询和报表的需求

那这样领域模型设计出来后,经常受到的挑战是查询性能不好。相比于以数据为中心的设计思维,以领域对象为中心的设计思维设计出的数据存储粒度更细,冗余更少,确实不利于查询。

另外一个对查询不友好的地方在于聚合这个概念。比如我想查一个订单列表,列表中的每一行只需要订单这个聚合根的数据,不需要把每一条订单下的子实体都被查询出来,那样性能会很差。

可以使用JPA提供的懒加载来实现只加载聚合根,但是会带来更多的其它的问题,懒加载已被认为是不被推荐使用的反模式。

大部分的业务系统都是读多写少,领域模型是重要,但能提供符合性能要求的查询也很重要。DDD是怎么解决这个矛盾的呢?

模型对查询不友好怎么办

DDD推荐使用CQRS( Command Query Responsibility Segregation, 命令职责分离)这种模式来解决这个问题。

https://martinfowler.com/bliki/CQRS.html

我们来看看IDDD_Sample的 com.saasovation.collaboration.application.forum 下的应用服务,这些服务分成了两类:

image.png

一类是 ApplicationService 一类是 QueryService 。

ApplicationService里的方法我们上篇文章里见过了,方法的入参是一个 Command 对象,返回值是一个 CommandResponse 对象。方法里通过 Repository 获取聚合根,操作聚合根后,再通过 Repository 来持久化。这就是CQRS里的"C",即命令(Command),它会改变系统的状态。

在collaboration这个上下文的实现里,没有把入参包装成Command对象,可以参考agilepm上下文中ApplicationService的实现。IDDD_Sample为了演示各种风格,不同上下文的实现方式不太统一。也有人是把这种ApplicationService里的一个个方法变成一个个CommandHandler,可读性更好,但本质上是一样的。

CQRS里的"Q"指的是Query,顾名思义,查询不会改变系统状态的。

那分离是什么意思呢?

最粗浅的理解是把这些查询方法放到 QueryService 里,而不是放到ApplicationService 里。但这只是个表象。我们看一个Query方法是怎么写的:

public ForumDiscussionsData forumDiscussionsDataOfId(String aTenantId, String aForumId) {         return this.queryObject(                 ForumDiscussionsData.class,                 "select "                 +  "forum.closed, forum.creator_email_address, forum.creator_identity, "                 +  "forum.creator_name, forum.description, forum.exclusive_owner, forum.forum_id, "                 +  "forum.moderator_email_address, forum.moderator_identity, forum.moderator_name, "                 +  "forum.subject, forum.tenant_id, "                 +  "disc.author_email_address as o_discussions_author_email_address, "                 +  "disc.author_identity as o_discussions_author_identity, "                 +  "disc.author_name as o_discussions_author_name, "                 +  "disc.closed as o_discussions_closed, "                 +  "disc.discussion_id as o_discussions_discussion_id, "                 +  "disc.exclusive_owner as o_discussions_exclusive_owner, "                 +  "disc.forum_id as o_discussions_forum_id, "                 +  "disc.subject as o_discussions_subject, "                 +  "disc.tenant_id as o_discussions_tenant_id "                 + "from tbl_vw_forum as forum left outer join tbl_vw_discussion as disc "                 + " on forum.forum_id = disc.forum_id "                 + "where (forum.tenant_id = ? and forum.forum_id = ?)",                 new JoinOn("forum_id", "o_discussions_forum_id"),                 aTenantId,                 aForumId);     }

来自 ForumQueryService

我们发现这个查询服务里即没有使用领域对象也没使用 Repository ,甚至 join 的两个表是代表两个聚合根的数据!在Query方法里可以直接查询数据库去返回一个查询显示用的DTO,这可以看做是分离的第一个意思,即Query里可以不使用领域对象和 Repository 。

我觉得可以不使用,意味着也可以使用

第二个意思是数据存储的分离。最简单的方式是处理 Command 的 ApplicationService/CommandHandler 访问的是主库,而 QueryService 访问的是从库。

在复杂一点,ApplicationService 可以访问MySQL,而 QueryService 可以访问Elasticsearch这种NoSQL,这时候需要做数据的同步。触发数据同步,可以监听领域事件来实现,也可以使用canal这种框架监听binlog来实现。除了专门为查询而设计的NoSQL本身有更好的查询性能,同时,我们在数据结构设计上也可以专门为查询来做优化,比如把多个关联聚合的数据放到一起。

image.png

其实,IDDD_Sample的例子中的 collaboration 这个上下文就是这么实现的——领域对象的存储使用的是LevelDB,查询用的是MySQL。

这种方式我很多年前就看到一个团队在用,他们也把Service分为WriteServcie和ReadService两种,但是并没有叫CQRS这个名字,也没有总结为一种模式,而是凭着经验这么做了。

事件溯源和CQRS

和CQRS经常一起出现的一种模式是事件溯源(Event Sourcing)。事件溯源是一种更"前卫"的模式,它不存储对象的状态,相反,存储影响其状态的所有事件。

需要查询对象的当前状态时,只要把所有事件回放一遍就能得到。但是,这种回放太昂贵了,所以可以保留一份对象的最新快照,这就需要和CQRS模式结合。

image.png

IDDD_Sample例子中的 collaboration 这个上下文实际上使用的就是事件溯源。我们看它的 Repository 实现,存储的是事件而不是实体本身:

事件溯源加上CQRS很"酷",但一般不建议使用,实现的成本太高。

public class EventStoreForumRepository         extends EventStoreProvider         implements ForumRepository {      @Override     public void save(Forum aForum) {         EventStreamId eventId =                 new EventStreamId(                         aForum.tenant().id(),                         aForum.forumId().id(),                         aForum.mutatedVersion());          this.eventStore().appendWith(eventId, aForum.mutatingEvents());     } }

即使是只使用CQRS这一模式,也建议循序渐进,从简单开始,最基本的,把QueryService 和 ApplicationService 分开,且在设计领域模型时不要受 QueryService 设计的影响。

以上是"如何使用CQRS避免查询对模型设计的影响"这篇文章的所有内容,感谢各位的阅读!相信大家都有了一定的了解,希望分享的内容对大家有所帮助,如果还想学习更多知识,欢迎关注行业资讯频道!

0