欢迎光临
我们一直在努力

DDD 战术设计:Repository 与持久化的边界

DDD 战术设计:Repository 与持久化的边界

目录

  • Repository
  • Repository 与 DAO
  • 接口与实现分离
  • 实战:OrderRepository
  • 小结

上一篇我们聊了领域事件。聚合根产生事件,订阅者根据自己的职责响应。但在业务执行过程中,经常需要加载已有的领域对象。比如支付订单前需要查询订单状态,修改订单后需要保存聚合状态。

问题是,如果领域模型直接依赖数据库访问代码,业务对象就会和具体的持久化技术绑定。换一个存储方案,领域对象也得跟着改。领域层关心的是"存什么、取什么",不应该关心"怎么存、怎么取"。Repository 就是为了解决这个隔离问题。

Repository

Repository(仓储) 是领域层中用于获取和保存聚合的抽象。它隐藏了底层的数据访问细节,让开发者可以像操作领域对象一样处理持久化。

你可以把 Repository 理解成图书馆的借阅台。你要借一本书,告诉工作人员书名,他帮你找到给你。你不需要知道这本书放在哪个架子的第几层,不需要知道管理员是用什么系统录入的,也不需要关心图书馆的仓库用的是铁柜子还是木柜子。你只需要说"我要这本书",然后拿到它。

Repository 对领域层做的事情一样。应用服务调 orderRepo.findById(orderId),拿到一个完整的 Order 聚合。这个调用背后可能查了数据库、可能读了缓存、可能做了对象重建,但领域层完全不感知。

// 领域层看到的接口,干干净净
public interface OrderRepository {
Order findById(String orderId);
void save(Order order);
List<Order> findByUserId(String userId);
}

这三个方法的签名里没有任何数据库相关的东西。没有 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 风格:调用方需要自己组装
Order order = orderDao.findById(orderId);
List<OrderItem> items = orderItemDao.findByOrderId(orderId);
order.setItems(items);

// Repository 风格:聚合作为整体返回
Order order = orderRepo.findById(orderId);
// OrderItem 已经在 Order 里了,不需要额外查询

Repository 把聚合内部的复杂性封装起来,对外只暴露一个完整的业务概念。这和上一篇聚合的设计思想是一致的:外部通过聚合根访问,不直接操作内部对象。

接口与实现分离

Repository 的接口定义在领域层,但实现在基础设施层。这是依赖倒置原则的体现:领域层定义"我需要什么能力",基础设施层提供"怎么实现这个能力"。实际项目中,有些团队也会把接口放在应用层,只要依赖方向保持一致(外层依赖内层,领域层不依赖基础设施层)就行,不必教条。

领域层(domain)
│
└── OrderRepository.java ← 接口定义
└── Order.java ← 聚合根

基础设施层(infrastructure)
│
└── MybatisOrderRepository.java ← MyBatis Plus 实现

领域层的 OrderRepository 只是一个接口:

// 领域层
public interface OrderRepository {
Order findById(String orderId);
void save(Order order);
List<Order> findByUserId(String userId);
}

基础设施层提供 MyBatis Plus 实现:

// 基础设施层
@Repository
public class MybatisOrderRepository implements OrderRepository {

@Autowired
private OrderMapper orderMapper;

@Override
public Order findById(String orderId) {
OrderDO orderDO = orderMapper.selectById(orderId);
return toDomain(orderDO);
}

@Override
public void save(Order order) {
OrderDO orderDO = toDO(order);
orderMapper.insertOrUpdate(orderDO);
}

@Override
public List<Order> findByUserId(String userId) {
List<OrderDO> list = orderMapper.selectList(
new QueryWrapper<OrderDO>().eq("user_id", userId)
);
return list.stream().map(this::toDomain).collect(Collectors.toList());
}

// 领域对象 ↔ 数据库对象的转换
private Order toDomain(OrderDO orderDO) { ... }
private OrderDO toDO(Order order) { ... }
}

注意 toDomain 和 toDO 这两个转换方法。它们是领域模型和持久化模型之间的桥梁。数据库对象(OrderDO)对应数据库表结构,可能有 MyBatis Plus 的 @TableName、@TableId 这些注解。但领域对象(Order)是纯粹的业务模型,不带任何持久化相关的注解。Repository 在中间做了翻译。

为什么要多这一层转换?因为数据库的表结构和业务模型关注的东西不一样。数据库里 order_item 可能是一张独立的表,有 order_id 外键关联。但在领域模型里,OrderItem 是 Order 聚合的内部对象,外部不应该直接访问。Repository 的实现负责处理这种结构差异。

这样做还有一个实际好处:换存储方案时,领域层不用动。 如果项目从 MySQL 迁到 MongoDB,只需要在基础设施层写一个新的 MongoOrderRepository 实现同一个接口,领域层的代码完全不受影响。同理,从 MyBatis Plus 换成其他持久化方案,也只是换实现层的代码。

// 如果以后换成 MongoDB
@Repository
public class MongoOrderRepository implements OrderRepository {

@Autowired
private MongoTemplate mongoTemplate;

@Override
public Order findById(String orderId) {
OrderDocument doc = mongoTemplate.findById(orderId, OrderDocument.class);
return toDomain(doc);
}

// …其他方法
}

应用服务注入的是接口,不关心具体是哪个实现。Spring 会自动注入当前配置的实现类。

实战:OrderRepository

把前面的内容串起来,看一个完整的 Order Repository 怎么设计。

接口定义在领域层,方法签名只有领域概念:

// 领域层
public interface OrderRepository {
Order findById(String orderId);
void save(Order order);
List<Order> findByUserId(String userId);
}

save 方法同时处理新增和更新。聚合根内部如果有新的 OrderItem(用户新加了商品),Repository 的实现负责保存;如果有被删除的 OrderItem,实现负责删除。调用方不需要区分"这是新建的订单还是已有的订单",直接 save 就行。

MyBatis Plus 实现在基础设施层:

// 基础设施层
@Repository
public class MybatisOrderRepository implements OrderRepository {

@Autowired
private OrderMapper orderMapper;

@Autowired
private OrderItemMapper orderItemMapper;

@Override
public Order findById(String orderId) {
OrderDO orderDO = orderMapper.selectById(orderId);
if (orderDO == null) {
throw new BusinessException("订单不存在");
}
List<OrderItemDO> items = orderItemMapper.selectList(
new QueryWrapper<OrderItemDO>().eq("order_id", orderId));
return toDomain(orderDO, items);
}

@Override
public void save(Order order) {
OrderDO orderDO = toDO(order);
orderMapper.insertOrUpdate(orderDO);
// 聚合内的子对象也由 Repository 负责保存
for (OrderItem item : order.getItems()) {
orderItemMapper.insertOrUpdate(toItemDO(item, order.getOrderId()));
}
}

@Override
public List<Order> findByUserId(String userId) {
List<OrderDO> list = orderMapper.selectList(
new QueryWrapper<OrderDO>()
.eq("user_id", userId)
.orderByDesc("create_time"));
return list.stream().map(orderDO -> {
List<OrderItemDO> items = orderItemMapper.selectList(
new QueryWrapper<OrderItemDO>().eq("order_id", orderDO.getOrderId()));
return toDomain(orderDO, items);
}).collect(Collectors.toList());
}
// 注意:实际项目中,列表查询通常会加分页(Page<Order>)或返回轻量级的摘要对象(OrderSummary),
// 而不是一次性加载所有聚合。大量聚合的列表查询更适合用专门的查询模型,这也是后面 CQRS 的核心思想。

private Order toDomain(OrderDO orderDO, List<OrderItemDO> itemDOs) {
Order order = Order.reconstruct(
orderDO.getOrderId(),
orderDO.getUserId(),
orderDO.getStatus(),
orderDO.getCreateTime()
);
for (OrderItemDO itemDO : itemDOs) {
order.addItem(
itemDO.getProductId(),
itemDO.getProductName(),
new Money(itemDO.getPrice(), "CNY"),
itemDO.getQuantity()
);
}
return order;
}
}

注意 toDomain 方法里用的是 Order.reconstruct() 而不是 Order.create()。create 是业务方法,会产生领域事件(比如 OrderCreatedEvent)。但 reconstruct 是从数据库还原对象,不是一次业务行为,不应该触发事件。这是领域对象的两种构造路径:一种是业务操作产生的(触发事件),一种是从持久化还原的(不触发事件)。

应用服务的使用方式:

// 应用层
public class OrderApplicationService {

private OrderRepository orderRepo; // 注入的是接口

public String placeOrder(PlaceOrderCommand cmd) {
Order order = Order.create(cmd.getUserId(), cmd.getDeliveryInfo());
for (OrderItemCommand item : cmd.getItems()) {
order.addItem(item.getProductId(), item.getProductName(),
item.getPrice(), item.getQuantity());
}

orderRepo.save(order);
return order.getOrderId();
}

public Order getOrderDetail(String orderId) {
return orderRepo.findById(orderId);
}
}

应用服务只和 Repository 接口打交道,不关心底层用的是 MyBatis Plus、JPA 还是 MongoDB。整个调用链:

应用服务
│
▼
OrderRepository(接口,领域层定义)
│
▼
MybatisOrderRepository(实现,基础设施层)
│
├── orderMapper.selectById() ← 查数据库
├── toDomain() ← DO → 领域对象
└── orderMapper.insertOrUpdate()← 写数据库
│
▼
数据库(MySQL / PostgreSQL / …)

小结

Repository 的核心思想,是让领域模型关注业务对象本身,而不是关注数据如何保存。它以聚合为边界进行加载和保存,让数据库结构和领域模型保持独立。接口在领域层,实现在基础设施层,两者通过 Repository 内部的转换逻辑解耦。

到这里,我们已经介绍了 DDD 中几个重要的战术设计元素:实体和值对象负责表达业务概念,聚合维护一致性边界,领域服务承载跨对象规则,领域事件负责业务解耦,Repository 隔离持久化细节。但这些对象在项目中应该如何组织?领域层、应用层、基础设施层之间如何划分职责?下一篇聊 DDD 分层架构。

赞(0)
未经允许不得转载:171主机测评 » DDD 战术设计:Repository 与持久化的边界
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址