DDD 仓储模式实战——Repository 和 DAO 的本质区别,以及何时用哪个
DDD 仓储模式实战——Repository 和 DAO 的本质区别,以及何时用哪个
适读人群:在用 DDD 或考虑引入 DDD 的 Java 工程师 | 阅读时长:约15分钟 | 核心价值:Repository 不是 DAO 的改名,它们解决不同的问题——搞清楚本质区别,才能做出正确选型
我被问到最多的 DDD 问题
我在很多技术交流群里聊 DDD,被问到最多的一个问题是:
"Repository 和 DAO 有什么区别?我看两个都是查数据库,为什么 DDD 要专门发明 Repository?"
这个问题问得很好,因为很多号称在用 DDD 的项目,Repository 和 DAO 根本就没有任何区别——Repository 接口里全是 findByXxx、countByYyy,和 Spring Data JPA 生成的接口一模一样,只是换了个名字。
DAO 是什么
DAO(Data Access Object)是数据访问对象,它的核心目标是:封装对数据库(或其他存储)的底层访问操作,让业务代码不需要直接写 SQL 或 JDBC。
DAO 的语义是面向存储的:
public interface OrderDAO {
Order findById(Long id);
List<Order> findByUserId(Long userId);
List<Order> findByStatusAndCreateTimeBetween(Integer status, Date start, Date end);
int countByStatus(Integer status);
void insert(Order order);
void update(Order order);
void deleteById(Long id);
}注意这些方法名:findByXxx、countByYyy、insert、update——这是数据库操作的语言,不是业务语言。
调用 DAO 的代码需要知道"我要按 status 和 create_time 查",这已经是在考虑数据库 Schema 了。
Repository 是什么
Repository(仓储)是 DDD 里的概念,它的核心目标是:让领域层能够以集合操作的方式来使用领域对象,屏蔽所有存储细节。
Repository 的语义是面向业务集合的:
public interface OrderRepository {
// 按业务 ID 取一个对象(业务语言)
Optional<Order> findById(OrderId orderId);
// 按业务语义查(不是按字段名,而是按业务含义)
List<Order> findPendingPaymentOrders(); // 查待支付的订单
List<Order> findOrdersRequiringShipment(); // 查待发货的订单
// 保存(不区分 insert 还是 update,Repository 自己判断)
void save(Order order);
// 从"集合"里移除
void remove(OrderId orderId);
}Repository 就像一个业务集合(或者说"集合容器"),调用方不需要知道对象存在哪里(MySQL?Redis?内存?),只需要知道"我从仓储里取一个对象,做完操作,再存回去"。
本质区别
| 维度 | DAO | Repository |
|---|---|---|
| 面向对象 | 面向数据库表 | 面向领域聚合 |
| 方法语义 | 数据操作(insert/update/findByXxx) | 业务语义(findPendingOrders) |
| 知道 Schema 吗 | 知道,方法名反映字段名 | 不知道,只知道业务概念 |
| 是否区分 insert/update | 区分 | 不区分,统一用 save |
| 是否暴露技术细节 | 是(Pageable、Criteria 等) | 否 |
| 适用层 | 任意层 | 只在领域层和应用层 |
为什么 save 要统一,不区分 insert/update
这是一个很多人会质疑的点:为什么 Repository 里只有 save,不区分新增和更新?
因为对领域层来说,我不应该关心对象是新的还是已有的。
业务逻辑只关心:我修改了这个订单的状态,我需要把修改持久化下去。至于这个订单是第一次保存还是第 N 次更新,这是基础设施层的事。
// 应用服务——不需要知道是 insert 还是 update
public void cancelOrder(OrderId orderId) {
Order order = orderRepository.findById(orderId).orElseThrow();
order.cancel(); // 修改聚合状态
orderRepository.save(order); // 让 Repository 决定 insert 还是 update
}Repository 的实现类(在基础设施层)来判断:
// Repository 实现(基础设施层)
@Repository
public class OrderRepositoryImpl implements OrderRepository {
@Override
public void save(Order order) {
OrderPO po = converter.toPO(order);
if (orderMapper.findById(po.getId()) == null) {
orderMapper.insert(po);
} else {
orderMapper.update(po);
}
// 发布领域事件
order.getDomainEvents().forEach(eventPublisher::publish);
order.clearDomainEvents();
}
}踩坑记录
踩坑一:Repository 接口里塞满了查询方法,成了 QueryDAO
我见过一个 Repository 接口有 40 多个方法:
public interface OrderRepository {
Optional<Order> findById(OrderId id);
List<Order> findByUserId(UserId userId);
List<Order> findByUserIdAndStatus(UserId userId, OrderStatus status);
List<Order> findByUserIdAndStatusAndCreateTimeBetween(...);
Page<Order> findPageByUserId(UserId userId, int page, int size); // Pageable 出现了!
long countByStatus(OrderStatus status);
BigDecimal sumAmountByUserIdAndCreateTimeBetween(...);
// 还有 30 个...
}这不是 Repository,这是带了 DDD 皮的 DAO。
根因:把 CQRS 的 Query 部分也放进了 Repository。
Repository 只应该服务于命令侧(Command),也就是需要修改领域状态的操作,它加载完整的聚合对象,执行业务操作,保存回去。
查询侧(Query),比如分页列表、统计数据、多表 JOIN 查询,这些应该走独立的查询服务(Query Service),直接查数据库,返回 DTO,不走领域层。
// 命令侧:Repository,返回领域对象
public interface OrderRepository {
Optional<Order> findById(OrderId id); // 用于命令操作
void save(Order order);
}
// 查询侧:Query Service,直接查 DTO
@Service
public class OrderQueryService {
@Autowired
private OrderMapper orderMapper; // 可以直接用 Mapper
public Page<OrderListDTO> queryOrderList(UserId userId, OrderQueryCondition condition) {
return orderMapper.selectPage(userId, condition); // 返回 DTO,不走领域对象
}
}踩坑二:Repository 的实现里做了业务判断
// 错误:Repository 实现里夹带了业务逻辑
public class OrderRepositoryImpl implements OrderRepository {
@Override
public void save(Order order) {
// 这是业务判断,不应该在 Repository 里!!!
if (order.getStatus() == OrderStatus.CANCELLED && order.getAmount().isGreaterThan(Money.of(1000))) {
alertService.sendHighValueCancellationAlert(order);
}
orderMapper.save(converter.toPO(order));
}
}Repository 实现里只能有存储相关的代码,不能有业务逻辑。业务判断放在领域层。
踩坑三:Repository 传参传了数据库字段名
// 错误:方法名暴露了数据库字段名
public interface OrderRepository {
List<Order> findByCreateTimeBetweenAndStatusIn(
LocalDateTime startTime, // create_time 字段
LocalDateTime endTime,
List<Integer> statusCodes // status 字段,用 Integer 表示
);
}
// 正确:用业务语言
public interface OrderRepository {
List<Order> findRecentOrders(DateRange range); // DateRange 是值对象
List<Order> findOrdersByStatuses(Set<OrderStatus> statuses); // 用领域枚举
}什么时候用 Repository,什么时候用 DAO
实践中,大多数系统里两者都需要:
- Repository:用于命令侧,加载聚合,执行业务,保存聚合
- Mapper / DAO:用于查询侧(放在 QueryService 里调用),返回 DTO,不走领域对象
Repository 的两种实现策略
策略 A:领域对象 = 持久化对象(简化版)
用 JPA,领域对象直接加 @Entity 注解,Repository 继承 JpaRepository。
优点:代码量少,开发快。
缺点:领域对象被 JPA 污染,有 @Entity、@Column 等注解,不够纯粹。ORM 的懒加载机制也会影响聚合的行为。
适用:团队规模小,业务复杂度不高,快速迭代优先。
策略 B:领域对象和持久化对象分离
领域对象(Order)不含任何 ORM 注解,持久化对象(OrderPO)负责和数据库打交道,Repository 实现里做转换。
优点:领域对象干净,不被基础设施污染,利于单元测试。
缺点:多了转换层,代码量增加约 30%。
适用:业务复杂,领域模型演进频繁,团队对 DDD 有一定理解。
最后的建议
不要为了用 Repository 而用 Repository。
如果你的系统是 CRUD 为主,用 MyBatis Mapper + 三层架构完全够了,强行引入 Repository 只会增加不必要的复杂度。
当你的业务逻辑开始复杂(一个操作涉及多个业务规则、状态机跳转、跨对象协调),开始考虑用 DDD,再引入 Repository。
