DDD 系列:仓储 Repository 详解

发布时间:2026/11/3 5:21:58

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/11/3 22:13:53

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

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

2026/10/25 2:22:13

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

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

2026/10/28 20:10:20

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

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

2026/11/4 1:43:38

训练效率翻倍:Moonlight-16B-A3B如何重塑大模型行业成本结构

训练效率翻倍:Moonlight-16B-A3B如何重塑大模型行业成本结构 【免费下载链接】Moonlight-16B-A3B 项目地址: https://ai.gitcode.com/MoonshotAI/Moonlight-16B-A3B 导语 月之暗面发布的Moonlight-16B-A3B模型通过Muon优化器实现训练效率跃升,5…

2026/11/4 1:43:38

Osiris入门指南:用Next.js 16 + MapLibre搭建全球OSINT仪表盘

Osiris入门指南:用Next.js 16 MapLibre搭建全球OSINT仪表盘 【免费下载链接】osiris Open Source Global Intelligence Platform - Real-Time OSINT Dashboard - A Palantir Alternative - 2nZNHm3Lr9umG3DVrzYwHgktwkuKuJRXqqRqs3ewpump 项目地址: https://git…

2026/11/3 7:47:06

面向复杂光照与角度的宠物面部识别鲁棒性增强策略

在智慧养宠时代,基于视觉的宠物身份识别技术正逐步从实验室走向家庭应用。然而,与配合度高的人类不同,宠物是典型的“非合作生物识别对象”——它们不会主动配合摄像头调整姿态,更无法控制光照环境。实际应用中,我们常…

2026/11/3 14:24:53

SSM电子书店源码全解析:结构、原理、部署与排错实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/11/4 0:03:20

AI辅助技术栈评估:提升研发决策效率60%

1. 项目背景与核心价值技术决策一直是研发团队面临的重大挑战。随着技术生态的快速演进,技术栈的选择、维护和迭代变得愈发复杂。我们团队在过去两年里,先后经历了三次重大技术栈迁移,每次都需要投入大量人力进行技术评估和风险分析。直到去年…

2026/11/4 0:03:20

从生成Token到直接决策:Jev如何重塑AI Agent工程实践

1. 从"生成文字"到"拍板决策":Jev 到底在解决什么问题大多数人第一次接触大模型,脑子里建立的模型是"输入问题、输出文字"。你问它今天天气怎么样,它给你一段描述;你让它写个周报,它给你…

2026/11/4 0:03:20

32G统一内存跑大模型:内存账、TPS实测与端云分流

想在一台 32G 统一内存的小主机上把大模型跑成"日常可用",这件事我前后折腾了小半年。标题里说的这台 Mac mini M6,是我们这次讨论的目标平台,具体到手规格以实物为准;但下面这套推算方法和实测思路,对任何统…

2026/11/3 7:47:06

面向复杂光照与角度的宠物面部识别鲁棒性增强策略

在智慧养宠时代,基于视觉的宠物身份识别技术正逐步从实验室走向家庭应用。然而,与配合度高的人类不同,宠物是典型的“非合作生物识别对象”——它们不会主动配合摄像头调整姿态,更无法控制光照环境。实际应用中,我们常…

2026/11/3 14:24:53

SSM电子书店源码全解析:结构、原理、部署与排错实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

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

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

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