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

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

[[355640]]

本文转载自微信公众号「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 设计的影响。

 

责任编辑:武晓燕 来源: codeasy
相关推荐

2011-06-16 13:36:01

Top查询

2013-01-17 14:34:49

微信移动应用产品设计

2024-01-23 10:35:09

ChatGPT人工智能

2010-03-02 16:21:02

Android设计平台

2011-08-01 10:00:43

数据中心冗余设计布线

2022-06-07 08:18:49

懒加载Web前端

2021-04-14 09:43:57

微服务拆分业务

2013-09-29 09:49:04

编程生活方式

2011-06-01 17:45:22

SEO

2015-11-04 14:03:44

BYOD自带设备企业

2009-03-20 09:56:00

CDMA软切换参数

2022-11-17 08:00:18

JavaScript错误性能

2014-07-22 09:25:48

LTEPTN4G

2022-09-27 15:00:18

物联网IoT

2011-05-24 16:01:51

OpenFlow影响

2023-09-05 15:48:00

2022-12-15 08:00:38

JavaScript错误性能

2021-10-13 22:38:42

数字货币货币人类

2013-04-26 10:14:20

交互设计

2019-09-19 14:17:59

数据中心云计算互联网
点赞
收藏

51CTO技术栈公众号