DDD 战术设计:Repository 与持久化的边界
DDD 战术设计Repository 与持久化的边界目录RepositoryRepository 与 DAO接口与实现分离实战OrderRepository小结上一篇我们聊了领域事件。聚合根产生事件订阅者根据自己的职责响应。但在业务执行过程中经常需要加载已有的领域对象。比如支付订单前需要查询订单状态修改订单后需要保存聚合状态。问题是如果领域模型直接依赖数据库访问代码业务对象就会和具体的持久化技术绑定。换一个存储方案领域对象也得跟着改。领域层关心的是存什么、取什么不应该关心怎么存、怎么取。Repository 就是为了解决这个隔离问题。RepositoryRepository仓储是领域层中用于获取和保存聚合的抽象。它隐藏了底层的数据访问细节让开发者可以像操作领域对象一样处理持久化。你可以把 Repository 理解成图书馆的借阅台。你要借一本书告诉工作人员书名他帮你找到给你。你不需要知道这本书放在哪个架子的第几层不需要知道管理员是用什么系统录入的也不需要关心图书馆的仓库用的是铁柜子还是木柜子。你只需要说我要这本书然后拿到它。Repository 对领域层做的事情一样。应用服务调orderRepo.findById(orderId)拿到一个完整的 Order 聚合。这个调用背后可能查了数据库、可能读了缓存、可能做了对象重建但领域层完全不感知。// 领域层看到的接口干干净净publicinterfaceOrderRepository{OrderfindById(StringorderId);voidsave(Orderorder);ListOrderfindByUserId(StringuserId);}这三个方法的签名里没有任何数据库相关的东西。没有 SQL没有 Mapper没有Select注解。领域层依赖的是这个接口不是某个具体的数据库实现。Repository 与 DAO很多人分不清 Repository 和 DAO。两者都在做数据存取但关注点完全不同。维度DAORepository面向什么数据库表聚合根关注点SQL 操作insert/update/delete业务概念save/findById返回什么数据行ResultSet、DTO领域对象聚合根所在层通常在 DAO 层或 Mapper 层接口在领域层实现在基础设施层一个对应几个表通常一张表对应一个 DAO一个聚合对应一个 Repository关键区别在面向什么。DAO 是面向表的一张order表对应一个OrderDao一张order_item表对应一个OrderItemDao。查一个订单要分别调两个 DAO再手动组装。Repository 是面向聚合的一个OrderRepository管的是整个 Order 聚合包括 OrderItem、DeliveryInfo。保存订单时Repository 负责保证 Order 聚合能够被完整保存至于内部对象如何映射到数据表由具体实现负责可能是 ORM 自动级联也可能是手动保存调用方不需要操心。// DAO 风格调用方需要自己组装OrderorderorderDao.findById(orderId);ListOrderItemitemsorderItemDao.findByOrderId(orderId);order.setItems(items);// Repository 风格聚合作为整体返回OrderorderorderRepo.findById(orderId);// OrderItem 已经在 Order 里了不需要额外查询Repository 把聚合内部的复杂性封装起来对外只暴露一个完整的业务概念。这和上一篇聚合的设计思想是一致的外部通过聚合根访问不直接操作内部对象。接口与实现分离Repository 的接口定义在领域层但实现在基础设施层。这是依赖倒置原则的体现领域层定义我需要什么能力基础设施层提供怎么实现这个能力。实际项目中有些团队也会把接口放在应用层只要依赖方向保持一致外层依赖内层领域层不依赖基础设施层就行不必教条。领域层domain │ └── OrderRepository.java ← 接口定义 └── Order.java ← 聚合根 基础设施层infrastructure │ └── MybatisOrderRepository.java ← MyBatis Plus 实现领域层的 OrderRepository 只是一个接口// 领域层publicinterfaceOrderRepository{OrderfindById(StringorderId);voidsave(Orderorder);ListOrderfindByUserId(StringuserId);}基础设施层提供 MyBatis Plus 实现// 基础设施层RepositorypublicclassMybatisOrderRepositoryimplementsOrderRepository{AutowiredprivateOrderMapperorderMapper;OverridepublicOrderfindById(StringorderId){OrderDOorderDOorderMapper.selectById(orderId);returntoDomain(orderDO);}Overridepublicvoidsave(Orderorder){OrderDOorderDOtoDO(order);orderMapper.insertOrUpdate(orderDO);}OverridepublicListOrderfindByUserId(StringuserId){ListOrderDOlistorderMapper.selectList(newQueryWrapperOrderDO().eq(user_id,userId));returnlist.stream().map(this::toDomain).collect(Collectors.toList());}// 领域对象 ↔ 数据库对象的转换privateOrdertoDomain(OrderDOorderDO){...}privateOrderDOtoDO(Orderorder){...}}注意toDomain和toDO这两个转换方法。它们是领域模型和持久化模型之间的桥梁。数据库对象OrderDO对应数据库表结构可能有 MyBatis Plus 的TableName、TableId这些注解。但领域对象Order是纯粹的业务模型不带任何持久化相关的注解。Repository 在中间做了翻译。为什么要多这一层转换因为数据库的表结构和业务模型关注的东西不一样。数据库里order_item可能是一张独立的表有order_id外键关联。但在领域模型里OrderItem 是 Order 聚合的内部对象外部不应该直接访问。Repository 的实现负责处理这种结构差异。这样做还有一个实际好处换存储方案时领域层不用动。如果项目从 MySQL 迁到 MongoDB只需要在基础设施层写一个新的MongoOrderRepository实现同一个接口领域层的代码完全不受影响。同理从 MyBatis Plus 换成其他持久化方案也只是换实现层的代码。// 如果以后换成 MongoDBRepositorypublicclassMongoOrderRepositoryimplementsOrderRepository{AutowiredprivateMongoTemplatemongoTemplate;OverridepublicOrderfindById(StringorderId){OrderDocumentdocmongoTemplate.findById(orderId,OrderDocument.class);returntoDomain(doc);}// ...其他方法}应用服务注入的是接口不关心具体是哪个实现。Spring 会自动注入当前配置的实现类。实战OrderRepository把前面的内容串起来看一个完整的 Order Repository 怎么设计。接口定义在领域层方法签名只有领域概念// 领域层publicinterfaceOrderRepository{OrderfindById(StringorderId);voidsave(Orderorder);ListOrderfindByUserId(StringuserId);}save方法同时处理新增和更新。聚合根内部如果有新的 OrderItem用户新加了商品Repository 的实现负责保存如果有被删除的 OrderItem实现负责删除。调用方不需要区分这是新建的订单还是已有的订单直接save就行。MyBatis Plus 实现在基础设施层// 基础设施层RepositorypublicclassMybatisOrderRepositoryimplementsOrderRepository{AutowiredprivateOrderMapperorderMapper;AutowiredprivateOrderItemMapperorderItemMapper;OverridepublicOrderfindById(StringorderId){OrderDOorderDOorderMapper.selectById(orderId);if(orderDOnull){thrownewBusinessException(订单不存在);}ListOrderItemDOitemsorderItemMapper.selectList(newQueryWrapperOrderItemDO().eq(order_id,orderId));returntoDomain(orderDO,items);}Overridepublicvoidsave(Orderorder){OrderDOorderDOtoDO(order);orderMapper.insertOrUpdate(orderDO);// 聚合内的子对象也由 Repository 负责保存for(OrderItemitem:order.getItems()){orderItemMapper.insertOrUpdate(toItemDO(item,order.getOrderId()));}}OverridepublicListOrderfindByUserId(StringuserId){ListOrderDOlistorderMapper.selectList(newQueryWrapperOrderDO().eq(user_id,userId).orderByDesc(create_time));returnlist.stream().map(orderDO-{ListOrderItemDOitemsorderItemMapper.selectList(newQueryWrapperOrderItemDO().eq(order_id,orderDO.getOrderId()));returntoDomain(orderDO,items);}).collect(Collectors.toList());}// 注意实际项目中列表查询通常会加分页PageOrder或返回轻量级的摘要对象OrderSummary// 而不是一次性加载所有聚合。大量聚合的列表查询更适合用专门的查询模型这也是后面 CQRS 的核心思想。privateOrdertoDomain(OrderDOorderDO,ListOrderItemDOitemDOs){OrderorderOrder.reconstruct(orderDO.getOrderId(),orderDO.getUserId(),orderDO.getStatus(),orderDO.getCreateTime());for(OrderItemDOitemDO:itemDOs){order.addItem(itemDO.getProductId(),itemDO.getProductName(),newMoney(itemDO.getPrice(),CNY),itemDO.getQuantity());}returnorder;}}注意toDomain方法里用的是Order.reconstruct()而不是Order.create()。create是业务方法会产生领域事件比如 OrderCreatedEvent。但reconstruct是从数据库还原对象不是一次业务行为不应该触发事件。这是领域对象的两种构造路径一种是业务操作产生的触发事件一种是从持久化还原的不触发事件。应用服务的使用方式// 应用层publicclassOrderApplicationService{privateOrderRepositoryorderRepo;// 注入的是接口publicStringplaceOrder(PlaceOrderCommandcmd){OrderorderOrder.create(cmd.getUserId(),cmd.getDeliveryInfo());for(OrderItemCommanditem:cmd.getItems()){order.addItem(item.getProductId(),item.getProductName(),item.getPrice(),item.getQuantity());}orderRepo.save(order);returnorder.getOrderId();}publicOrdergetOrderDetail(StringorderId){returnorderRepo.findById(orderId);}}应用服务只和 Repository 接口打交道不关心底层用的是 MyBatis Plus、JPA 还是 MongoDB。整个调用链应用服务 │ ▼ OrderRepository接口领域层定义 │ ▼ MybatisOrderRepository实现基础设施层 │ ├── orderMapper.selectById() ← 查数据库 ├── toDomain() ← DO → 领域对象 └── orderMapper.insertOrUpdate()← 写数据库 │ ▼ 数据库MySQL / PostgreSQL / ...小结Repository 的核心思想是让领域模型关注业务对象本身而不是关注数据如何保存。它以聚合为边界进行加载和保存让数据库结构和领域模型保持独立。接口在领域层实现在基础设施层两者通过 Repository 内部的转换逻辑解耦。到这里我们已经介绍了 DDD 中几个重要的战术设计元素实体和值对象负责表达业务概念聚合维护一致性边界领域服务承载跨对象规则领域事件负责业务解耦Repository 隔离持久化细节。但这些对象在项目中应该如何组织领域层、应用层、基础设施层之间如何划分职责下一篇聊 DDD 分层架构。