当前位置:主页 > 查看内容

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

发布时间:2021-09-18 00:00| 位朋友查看

简介:本文转载自微信公众号「codeasy」,作者阎华。转载本文请联系codeasy公众号。 使用了DDD(领域驱动设计)后,代码编写有什么不一样呢?这可能是程序员们在接触DDD后最关心的一个问题。这个系列文章会对一些优秀的DDD实例代码进行分析,管中窥豹,略见数斑。这是……

本文转载自微信公众号「codeasy」,作者阎华。转载本文请联系codeasy公众号。 

使用了DDD(领域驱动设计)后,代码编写有什么不一样呢?这可能是程序员们在接触DDD后最关心的一个问题。这个系列文章会对一些优秀的DDD实例代码进行分析,管中窥豹,略见数斑。这是第四篇,继续以IDDD_Sample为例做分析。

今天我们说一下“读”类型的应用服务怎么设计,以及如何获得更好的性能。

模型设计时不要考虑查询

按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方法是怎么写的:

  1. public ForumDiscussionsData forumDiscussionsDataOfId(String aTenantId, String aForumId) { 
  2.         return this.queryObject( 
  3.                 ForumDiscussionsData.class, 
  4.                 "select " 
  5.                 +  "forum.closed, forum.creator_email_address, forum.creator_identity, " 
  6.                 +  "forum.creator_name, forum.description, forum.exclusive_owner, forum.forum_id, " 
  7.                 +  "forum.moderator_email_address, forum.moderator_identity, forum.moderator_name, " 
  8.                 +  "forum.subject, forum.tenant_id, " 
  9.                 +  "disc.author_email_address as o_discussions_author_email_address, " 
  10.                 +  "disc.author_identity as o_discussions_author_identity, " 
  11.                 +  "disc.author_name as o_discussions_author_name, " 
  12.                 +  "disc.closed as o_discussions_closed, " 
  13.                 +  "disc.discussion_id as o_discussions_discussion_id, " 
  14.                 +  "disc.exclusive_owner as o_discussions_exclusive_owner, " 
  15.                 +  "disc.forum_id as o_discussions_forum_id, " 
  16.                 +  "disc.subject as o_discussions_subject, " 
  17.                 +  "disc.tenant_id as o_discussions_tenant_id " 
  18.                 + "from tbl_vw_forum as forum left outer join tbl_vw_discussion as disc " 
  19.                 + " on forum.forum_id = disc.forum_id " 
  20.                 + "where (forum.tenant_id = ? and forum.forum_id = ?)"
  21.                 new JoinOn("forum_id""o_discussions_forum_id"), 
  22.                 aTenantId, 
  23.                 aForumId); 
  24.     } 

来自 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很“酷”,但一般不建议使用,实现的成本太高。

  1. public class EventStoreForumRepository 
  2.         extends EventStoreProvider 
  3.         implements ForumRepository { 
  4.  
  5.     @Override 
  6.     public void save(Forum aForum) { 
  7.         EventStreamId eventId = 
  8.                 new EventStreamId( 
  9.                         aForum.tenant().id(), 
  10.                         aForum.forumId().id(), 
  11.                         aForum.mutatedVersion()); 
  12.  
  13.         this.eventStore().appendWith(eventId, aForum.mutatingEvents()); 
  14.     } 

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


本文转载自网络,原文链接:https://mp.weixin.qq.com/s/H-nWn2qLom1tQGSFHDMa5A
本站部分内容转载于网络,版权归原作者所有,转载之目的在于传播更多优秀技术内容,如有侵权请联系QQ/微信:153890879删除,谢谢!

推荐图文


随机推荐