如何在DDD中正确实现Repository?关系型数据库适配难题求解
针对DDD聚合根持久化(从NoSQL到关系型数据库)的解决方案
核心原则:Repository封装持久化细节,领域层与存储实现解耦
不管底层是NoSQL单文档还是关系型分表,领域层和应用层都不应该感知存储方式的差异,所有适配逻辑都应该由Repository层承担。以下是具体思路:
1. 让聚合根追踪自身变更(推荐方案)
在Report聚合根内部维护内存级的变更集合,用于记录本次操作中新增/修改/删除的Transaction:
- 在
Report中添加private List<Transaction> addedTransactions;(仅内存存在,不持久化到数据库) - 当调用领域方法
report.addTransaction(transaction)时,除了将交易加入主transactions集合,同时将其加入addedTransactions - Repository的
save(Report report)方法内部逻辑:- 先更新
Report主表的信息(若有变更) - 遍历
addedTransactions,批量插入到Transactions表 - 清空
addedTransactions,避免重复提交
- 先更新
这种方式下,应用层依然只需要调用reportRepository.save(report),领域层只负责业务规则校验(比如交易金额是否符合Report的限制),完全不需要关心底层是单文档还是分表存储。
2. 重新理解CRUD仓库的“更新”操作
CRUD仓库的update不等于“全量覆盖”,它的本质是持久化聚合根的当前状态。在关系型数据库下,Repository可以内部实现增量更新:
- 当加载
Report聚合时,同时加载关联的Transactions到内存 - 保存时,对比内存中
Transactions集合与数据库中的数据(或通过变更追踪标记),仅执行必要的插入/更新/删除操作 - 所有对比、增量计算逻辑都封装在Repository内部,领域层和应用层完全无感知
3. 优化方案2:领域事件与持久化解耦
如果想用领域事件,注意不要让持久化逻辑依赖事件:
- 领域层在完成
addTransaction操作后,发布TransactionAdded事件,用于业务逻辑的延伸(比如触发报表计算、通知其他模块) - 持久化操作依然由Repository的
save方法统一处理,事件只是业务协作的手段,不是驱动持久化的工具 - 这种方式避免了因为存储变更而修改领域事件的定义,符合DDD关注点分离的原则
4. 聚合大小与CRUD仓库的关系
CRUD仓库并非仅适用于小型聚合,关键是聚合边界是否合理:
- 只要
Report作为聚合根的边界是正确的(比如Transaction的生命周期完全依赖Report,所有对Transaction的操作必须通过Report),无论聚合内包含多少实体,都可以用CRUD仓库 - 你的问题本质不是聚合大小,而是Repository的实现没有封装好存储层的差异,导致上层逻辑被迫适配存储方式
内容的提问来源于stack exchange,提问作者Giorgos Ntymenos
相关产品推荐
相关产品推荐

