DDD 系列:仓储 Repository 详解

发布时间:2026/9/15 17:29:11

DDD 系列:仓储 Repository 详解 上一篇文章中我们把应用服务、聚合根和领域服务的职责分清楚了。应用服务负责完成一次用例聚合根负责守住业务规则领域服务处理无法自然归属单个对象的领域规则。代码写到这里很快会遇到下一个问题应用服务需要先拿到聚合再调用业务方法最后保存聚合。这个“拿到”和“保存”到底应该怎么做领域对象要不要直接调用 Mapper这就是仓储要解决的问题。Repository 是领域层访问聚合的抽象它让上层按领域对象思考而不是按表、SQL 和 ORM 框架思考。这一篇只讲仓储本身。至于项目具体怎么分包放到第 12 章再统一整理。一、为什么需要仓储先看一个没有仓储的订单支付方法Transactionalpublicvoidpay(StringorderNo){OrderDOorderDOorderMapper.selectByOrderNo(orderNo);OrderorderorderConverter.toDomain(orderDO);order.pay();orderMapper.updateById(orderConverter.toData(order));}这段代码不算错但应用服务已经知道了 Mapper、DO 和数据库更新方式。以后从 MyBatis 换成 JPA或者订单需要同时加载明细表应用服务都要跟着调整。更麻烦的是业务代码很容易慢慢退回“查一条表记录、改几个字段、执行 update”的数据表思维。仓储把这部分细节收起来Transactionalpublicvoidpay(StringorderNo){OrderorderorderRepository.findByOrderNo(orderNo).orElseThrow(()-newIllegalArgumentException(订单不存在));order.pay();orderRepository.save(order);}应用服务只关心三件事加载订单、执行支付、保存订单。至于订单来自几张表、使用什么 ORM、如何组装对象交给仓储实现处理。二、仓储不是 DAO 换了个名字DAO 或 Mapper 通常围绕数据表工作仓储则围绕聚合工作。对比项DAO / MapperRepository面向对象表记录、DO聚合根常见方法selectById、updateByIdfindByOrderNo、save返回结果DO、Map、数据行完整聚合关注点SQL 和持久化操作领域对象的获取与保存所在语义基础设施语义领域语义仓储底层完全可以继续使用 Mapper但不应该把 Mapper 直接暴露给领域层和应用层。如果一个所谓的仓储仍然提供updateStatus、selectPage、deleteByCondition这类表操作方法它多半只是换了名字的 DAO。三、仓储接口和实现怎么写1. 接口围绕聚合根定义仓储接口表达的是领域需要什么能力所以通常跟聚合放在同一个领域模块中。/** * 订单仓储 * * 该接口只描述订单聚合的获取和保存能力不暴露 SQL、分页插件等技术细节。 */publicinterfaceOrderRepository{/** * 根据订单编号加载完整订单聚合 * * 返回的订单必须包含执行业务行为所需的数据不能只返回一条不完整的订单主表记录。 */OptionalOrderfindByOrderNo(StringorderNo);/** * 保存订单聚合 * * 由仓储实现负责识别新增或更新并持久化聚合边界内需要同步保存的数据。 */voidsave(Orderorder);}接口名称要使用业务语言。findByOrderNo比selectOne更能说明调用意图。2. 实现负责持久化细节仓储实现放在基础设施层可以依赖 MyBatis、JPA、数据库表对象和转换器。RepositorypublicclassMyBatisOrderRepositoryimplementsOrderRepository{privatefinalOrderMapperorderMapper;privatefinalOrderItemMapperorderItemMapper;privatefinalOrderDataConverterorderDataConverter;/** * 加载订单主表和明细表并恢复成能够执行业务行为的完整聚合。 */OverridepublicOptionalOrderfindByOrderNo(StringorderNo){OrderDOorderDOorderMapper.selectByOrderNo(orderNo);if(orderDOnull){returnOptional.empty();}ListOrderItemDOitemDOListorderItemMapper.selectByOrderId(orderDO.getId());returnOptional.of(orderDataConverter.toDomain(orderDO,itemDOList));}/** * 将聚合拆成数据库模型后保存。 * * 这里必须在同一事务内维护订单主表和明细表的一致性调用方不需要知道具体表结构。 */Overridepublicvoidsave(Orderorder){OrderDOorderDOorderDataConverter.toOrderDO(order);orderMapper.saveOrUpdate(orderDO);orderItemMapper.replaceByOrderId(orderDO.getId(),orderDataConverter.toItemDOList(orderDO.getId(),order.getItems()));}}下面这张图把完整链路串起来。仓储接口属于领域语义仓储实现和 Mapper 才负责技术细节。四、领域模型和数据模型怎么转换领域对象和数据库对象通常不是一一对应的。一个Order聚合可能对应订单主表、订单明细表和多个值对象反过来一张宽表也可能被转换成多个领域概念。所以不要依赖BeanUtils.copyProperties()假装两者完全相同。/** * 订单持久化模型转换器 * * 负责数据库模型与领域模型之间的转换所有字段都要按业务含义显式映射。 */publicclassOrderDataConverter{/** 将数据库记录恢复成订单聚合 */publicOrdertoDomain(OrderDOorderDO,ListOrderItemDOitemDOList){MoneytotalAmountnewMoney(orderDO.getTotalAmount(),orderDO.getCurrency());ListOrderItemitemsitemDOList.stream().map(this::toDomainItem).toList();returnOrder.restore(newOrderId(orderDO.getOrderNo()),orderDO.getBuyerId(),items,totalAmount,OrderStatus.valueOf(orderDO.getStatus()),orderDO.getVersion());}}这里使用restore是为了区分两种动作create表示新建订单需要执行新建规则并产生相应行为restore表示从存储中恢复已经存在的订单不能再次触发“订单创建”规则。从数据库恢复对象不等于创建一个新业务对象。这两个入口混在一起很容易重复产生领域事件或覆盖历史状态。五、保存聚合时要注意什么1. 一个仓储对应一个聚合根外部只通过聚合根仓储加载和保存聚合。订单明细属于订单聚合内部时不需要再提供一个能被应用服务随意调用的OrderItemRepository。否则外部就可能绕过Order直接修改订单明细聚合边界也就失效了。2. 事务通常放在应用服务应用服务知道一次用例从哪里开始、到哪里结束因此通常由它控制事务。仓储负责保存不负责决定整条用例的事务范围。TransactionalpublicvoidchangeAddress(ChangeOrderAddressCommandcommand){OrderorderorderRepository.findByOrderNo(command.orderNo()).orElseThrow(()-newIllegalArgumentException(订单不存在));order.changeAddress(command.address());orderRepository.save(order);}3. 并发更新要有保护两个请求同时加载同一订单时都可能基于旧状态修改。常见做法是给聚合增加版本号并在更新 SQL 中带上旧版本条件UPDATEordersSETstatus#{status},versionversion1WHEREorder_no#{orderNo}ANDversion#{version};如果影响行数为 0说明数据已经被其他请求修改应该抛出并发冲突异常而不是静默覆盖。仓储保存的不只是数据还要维护聚合在并发场景下的一致性。六、查询一定要走仓储吗不一定。仓储适合加载需要执行业务行为的聚合。列表、报表、联表详情等只读查询如果也强行恢复完整聚合代码会很重性能也未必合适。这类场景可以单独定义查询服务直接返回页面需要的 DTO 或 VOpublicinterfaceOrderQueryService{/** 按页面条件查询订单列表只返回展示所需字段 */PageResultOrderListVOpage(OrderPageQueryquery);}这已经带有 CQRS 的思路修改操作通过领域模型查询操作使用更适合读取的模型。第 16 章会把 CQRS 和其他解耦方案放到一起比较这里先不展开。七、怎么验证仓储是否合理可以从三层测试入手用普通单元测试验证转换器确认 DO 能恢复成完整聚合值对象和状态没有丢失用数据库集成测试验证仓储保存后重新加载聚合关键字段应保持一致用应用服务测试验证调用顺序业务失败时不保存成功时只保存一次。还可以直接做一次代码检查应用层是否还出现 Mapper、DO 和 SQL 条件领域层是否依赖 MyBatis、JPA 注解或数据库框架仓储是否按聚合加载而不是只返回残缺主表数据查询接口是否为了“统一”而强行恢复聚合。看到应用服务只使用OrderRepository和Order基础设施实现可以独立替换说明边界基本是清楚的。八、总结这一篇把 Repository 的落地方式整理了一遍。仓储接口面向聚合仓储实现面向数据库应用服务只看领域对象Mapper 和 DO 留在基础设施层。同时还要记住仓储不是 DAO 改名一个聚合根对应一个仓储保存时要考虑事务和并发只读查询也不必强行走聚合。下一篇继续学习领域事件。聚合完成业务行为之后其他聚合或外部模块怎么知道发生了什么就要靠事件来协作了。
延伸阅读

更多相关文章

2026/9/15 17:28:25

深入解析TMS320F2802x I2C寄存器:从时钟配置到FIFO应用实战

1. 项目概述与I2C总线核心价值在嵌入式系统开发中,设备间的通信是构建复杂功能的基础。面对琳琅满目的通信协议,I2C(Inter-Integrated Circuit)总线以其简洁的两线制(串行数据线SDA和串行时钟线SCL)、支持多…

2026/9/12 2:31:33

Python数据科学工具链全解析:从NumPy到深度学习

1. 数据科学领域的Python生态全景Python在数据科学领域已经形成了完整的工具链生态,从基础数值计算到高级机器学习,每个环节都有成熟的库支持。根据2023年PyPI官方统计,数据科学相关库的月下载量已突破8亿次,其中NumPy、Pandas和M…

2026/9/12 9:31:32

Excel正则表达式匹配:用XLOOKUP和FILTER实现复杂模式查找

在实际数据处理和报表生成中,Excel 的查找功能经常遇到一个瓶颈:VLOOKUP 或 INDEX/MATCH 只能做精确匹配或简单通配符匹配。当需要按特定模式(如手机号格式、邮箱规则、产品编码规律)查找时,往往需要借助辅助列或复杂公…

2026/9/15 17:28:08

Python实现海洋SSTA的EOF分析全流程:从数据下载到物理解读

1. 为什么用EOF分析SSTA不是“炫技”,而是解决真问题的必要手段你有没有遇到过这样的情况:手头有一堆全球海表温度异常(SSTA)的NetCDF文件,时间跨度几十年,空间分辨率是11,变量维度是(time, lat…

2026/9/15 17:28:08

VisionMaster本地图像模块:离线调试机器视觉方案的实用指南

做机器视觉方案调试,最烦的事情是什么?我个人觉得,不是算法效果差,而是每次改完参数都要跑一遍产线,等相机重新拍图。尤其是项目前期,相机还没装好、或者现场图片没批量传出来的时候,整个调试进…

2026/9/15 17:28:08

CSR-DCF目标跟踪算法源码运行全指南:从原理到调参避坑

进入计算机视觉这个圈子,尤其是视频目标跟踪方向的人,基本都绕不开CSR-DCF这个名字。它是2017年CVPR上的工作,全称是Discriminative Correlation Filter with Channel and Spatial Reliability,也就是带通道可靠性和空间可靠性的判…

2026/9/15 17:23:07

SQL Server AlwaysOn可用性组从零部署:高可用架构实战指南

我有个习惯,技术圈里只要刮起“部署”风,我总会先往数据库这层瞟一眼。这不,最近大家聊本地部署聊得火热,从大模型到Docker再到CI/CD自动部署,人人都能把几十个容器跑起来,但真正轮到底层数据库的高可用部署…

2026/9/15 4:54:30

拯救者Y7000黑屏故障排查与维修实战指南

1. 项目概述:一台黑屏的拯救者Y7000,到底卡在哪一步? 联想拯救者Y7000系列笔记本,从2018年第一代搭载i5-8300H开始,到后来的i7-9750H、i7-10750H、i5-11400H,再到2023年款的R7-7840HS,它始终是学…

2026/9/15 0:01:16

AI英语单词APP开发:自适应学习算法与移动端优化实践

1. 项目概述 作为一名在移动应用开发领域摸爬滚打多年的老手,我最近完成了一个AI英语单词APP的开发项目。这个项目将传统单词记忆方法与现代AI技术相结合,打造了一款能够智能适应不同用户学习习惯的英语学习工具。 市面上大多数单词APP都存在一个通病&a…

2026/9/15 0:01:16

Flutter与OpenHarmony结合开发手语学习APP实战

1. 项目背景与核心价值作为一名同时接触过Flutter和OpenHarmony的开发者,最近我完成了一个基于Flutter for OpenHarmony的手语学习APP实战项目。这个项目最大的特点在于实现了跨平台框架与国产操作系统深度结合的创新实践——用Flutter开发的应用能完美运行在OpenHa…

2026/9/15 0:01:16

六个月成为机器人工程师:从ROS2到SLAM的实战路径

1. 六个月的紧迫感从哪来:先搞清楚你要成为哪种机器人工程师说实话,六个月的期限并不是一个宽松的时间线。市面上任何一本正经的机器人学教材都超过五百页,ROS2的官方文档可以翻到你怀疑人生,再加上ABB、KUKA这些工业机器人厂家动…

2026/9/15 14:22:53

USB Type-C PCB布局分区设计:电源、高速信号与PD协议全攻略

做硬件这行,Type-C接口算是典型的“看着简单,做起来全坑”的东西。光引脚就24个,高低速信号、电源、控制线全部塞在一个小小的连接器里,如果PCB布局不做规划,打样回来基本就是“插上没反应”、“高速掉线”、“静电一打…

2026/9/14 13:53:59

系统编程学习原型如何补齐稳定性边界

系统编程学习原型如何补齐稳定性边界预算有限时&#xff0c;我先优化明显多余的复制&#xff0c;而不是猜测性地换容器。用借用传递只读数据通常就能减少分配&#xff1a; fn parse(line: &str) -> Result<Item, Error> { /* ... */ }用基准确认热点确实在分配&am…

2026/9/15 11:42:23

雨花区哪家财务公司代理记账比较好?

在雨花区&#xff0c;企业处理财税事务常常面临诸多挑战&#xff0c;选择一家靠谱的财务公司至关重要。湖南巨勤财务管理咨询有限公司就是本地正规实体财税服务机构&#xff0c;深耕本地工商财税行业多年&#xff0c;熟悉当地工商局、税务局最新政策与申报流程。主营公司注册、…

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
咨询二维码