前言
CQRS在领域驱动设计中非常常见,也非常好用,简单来说就是对领域模型的修改和对领域生成数据的读取职责是相互分离的。
CQRS分类
CQRS在落地过程中分为几种
第一种是领域模型通过仓库的具体实现把数据保存在数据库中,这个是由写模型完成的。读模型承担的是直接读取数据库用于前端或者其他端的报表数据查询,绕过仓库直接查询数据库。可见,第一种方式是读和写在存储层上层分离,但共用存储
第二种是写模型和读模型在存储层也是分离的,写模型写入数据后会发布领域事件,领域事件的消费者会消费领域事件并把数据写入到其他存储介质,这些介质可以是缓存,数据库,搜索等,数据结构也是为了查询方便做适配。
第三种是标准的event source,写模型会记录每一个写入事件,模型存储也是通过记录这些事件实现,类似于redis aof存储,当取模型的时候把之前的事件重放一下就可以构建这个领域模型。读模型依然与第二种相同。
基于第二种与第三种又分为相同进程消费领域事件和不同进程消费领域事件。原理大同小异,这里不过多介绍。
在ddd实践过程中,针对项目大小决定选用哪种实现方式,我们落地的项目不大,且只设计了一个核心领域的限界上下文,因此采用了第一种实现方式,即写模型走rwpository仓库,读模型走传统mapper,读写共用数据库。repository只有save delete getbyid等集合类应该有的方法,除此之外没有任何其他方法,这也是领域驱动设计cqrs的要求。
落地过程中的问题
在落地过程中有一个非常矛盾的问题,即我们的领域模型聚合,有一个非常核心的方法,就是计算功能,业务上需要取出所有状态为活跃的聚合,然后拿这些活跃的聚合分别做实时计算。
在这种场景下,如果拿写模型实现,写模型有个要求就是不能有返回值,并且写模型都是对领域模型的修改,这显然与我们取出计算这种只读的业务需求不符
如果拿读模型实现,读模型要求绕过repository,直接读存储层,这与我们需要过滤模型并拿模型进行计算也不符
再三探索下我们发现一个好方法,即把这种需要按状态过滤出多个聚合并拿这些聚合做计算这个显著的过程封装成领域服务,由领域服务的实现类来过滤出合适的聚合,此处可以是读缓存或者读数据库来获取到这些聚合,因为模型不会更改聚合,所以可以随意使用,然后拿这些聚合做计算返回计算结果给应用层,应用层返回结果给上层的读模型接口。这个过程绕过了repository,直接读存储解决了问题。
实际上这种多个聚合配合完成一个领域逻辑的过程一般都应该封装在领域服务内,由于我们初次实践,没有这种意识,所以困扰许久。