笔者最近一直在学习DDD
,包括看完几本领域驱动设计TOP
的书籍,以及极客时间的《DDD
实战课》专栏,并且也看了很多demo
案例。有趣的是,由于DDD
并不像MVC
架构那样有统一的模版,每个作者实现的架构风格截然不同,这也给我提供了很多参考价值,感谢这些大佬!
若要我推荐的话,我更乐意向大家推荐《领域驱动设计(Thoughtworks
洞见)》这本书,这本书案例很多,对于初学者来说更容易理解DDD
,这本书给我的启发很大。(我并不推荐大家使用四层架构,而更喜欢《领域驱动设计(Thoughtworks
洞见)》这本书中DEMO
的架构风格)
作为初学者,我尝试将新项目架构改为DDD
四层架构后,遇到很多问题,性能问题是一方面,另一方面我也一直在思考如何优雅的实现查询,诸如报表统计类的查询(含分页查询)。也因为是应用在实际项目中,所以才一直逼着自己去解决这些问题,项目架构也每天在调整。
我看了很多书、网上找资料,其实就是想看看别人都是如何实现查询功能的,特别是分页报表类的查询、以及在查询性能方面的考虑。
其实是自己一开始就将思维限制在了死胡同,将思考方向固定在通过聚合根/领域服务实现,方向一错就很难找到答案,为此我付出了很多时间不断的调整代码、不断的调整代码。
最后在《领域驱动设计(Thoughtworks
洞见)》这本书中找到了我想要的答案。
根据书中给出的三种实现方式,笔者综合我们项目当前使用的DDD
经典四层架构现状,在四层架构的基础上做了些许改动。
优化报表统计分页查询流程如下:
接口层接收请求 -> 应用层处理请求 -> 直接调用DAO查询
这不就是MVC
三层架构吗?是,也不是!
报表统计一般会查询OLAP
数据库,即便没有将数据同步到一个分析型的数仓,也应该会将数据同步到一个从库,或者由定时任务按天/按月统计数据,将统计数据生成一个新的表。在数据同步的过程,为了降低数据存储的成本,也为提升查询的性能,经常需要将原表数据统计/删减后再存入一个新表/从库/分析型数据库,并且数据不会再修改。
可见,数据查询分析并不需要处理业务逻辑,在DDD
建模时,也不会考虑数据分析的情况,所以数据分析应该绕过领域建模,绕过聚合根、Repository
,直接从数据库读取数据。最好的方式当然是将报表统计数据分析放到一个独立的项目去做。
CQRS
(Command Query Resposibility Segregation
),即命令查询职责分离,软件模型中存在读模型和写模型之分,以我们写业务代码的经验也知道,一次请求,要么是作为一个“命令”执行一次操作,要么作为一个”查询“向调用方返回数据,两者不可能共存。CQRS
是将“命令”和“查询”分别使用不同的对象模型来表示。
共享存储指同一个表结构存储数据,共享模型指使用聚合根从数据库读取数据。
例如查询订单详情,订单的聚合根为Order
。
// 订单聚合根
public class Order extends BaseAggregate {
public OrderRepresentation toRepresentation() {
}
}
在应用层OrderApplicationService
通过OrderRepository
查询订单聚合根,再调用toRepresentation
将聚合根转为读模型。
public class OrderApplicationService {
public OrderRepresentation byId(String id) {
Order order = orderRepository.byId(id);
return order.toRepresentation();
}
}
Order
聚合根的toRepresentation
方法是将Order
转为读模型实体,屏蔽一些信息。在经典四层架构中,由应用层专门的一个类负责将聚合根转为读模型实体(这里是DTO
),也就是将DO
转为DTO
,在采用CQRS
后,Order
聚合根为写模型,OrderRepresentation
为读模型,对应DTO
。
笔者采用的是极客时间《DDD
实战课》推荐的经典四层架构,应用读模型还是使用DTO
,toRepresentation
由assembler
(装配工)完成。
为了更好的区分读和写,OrderApplicationService
应该只负责写请求,而创建新的OrderRepresentationService
负责读请求(并非所有读请求),接口层的OrderController
同时依赖OrderApplicationService
与OrderRepresentationService
。
共享存储-读写分离模型指读写还是操作同一张表,只是写模型与读模型不同,写通过聚合根操作,而读模型绕过聚合根、Repository
,直接操作数据库,此时的读模型就是用于装载从数据库查询的数据,并且不需要再作转换就可以响应给调用方,这里的读模型相当于DTO
。
对于单个聚合根内的查询,使用此模型可以应付复杂的查询场景,并且可以提升性能。例如,查询订单信息时,我们可能并不需要获取订单下的每个子订单OrderItem
。
对于需要跨多个聚合根的查询,共享模型无法实现此需求场景,而分别查询多个聚合根后,再合并查询结果不仅是将原本简单的事情变复杂,还大大影响性能,因此更有必要采用读写分离模型。
例如查询订单详情希望带上商品信息。
如果商品与订单在同一个服务,并且同一个数据库,那么便可以使用join
多表查询。
如果商品是一个微服务、订单是一个微服务,并且两个微服务使用不同的数据库,那么就只能是分别查询多个聚合根后,再合并查询结果。对于此场景,如果要提升性能,就需要通过额外的数据同步服务,将订单与商品查询结果合并后存入一个新的表,或者是存储到NoSQL
数据库。
CQRS
的读操作放在四层架构(应用层、领域层、基础架构层、接口层)中的应用层,而不是领域层,因为读操作不仅仅只是查询数据库,更多时候也会查询缓存、ES
。领域层负责写、缓存数据同步通过消费领域事件方式同步。