你写的 DAO 越来越像万能类——Repository 模式跟 DAO 差了一个抽象层
Java 后端项目里,DAO 和 Repository 这两个词经常被混着用,甚至很多团队里就是同一个东西两个名字。
但我最近重构一个 3 年老项目时发现,这俩在抽象层级上完全不是一回事。当你把所有数据访问都叫 DAO 时,你的 DAO 就会越写越大,最后变成"什么都能查、什么都能改"的万能类,业务逻辑和数据访问混在一起,改一行 SQL 动 20 个文件。
Repository 模式不是给 DAO 换名字,是把数据访问的抽象层级从"行"升到"集合"。
DAO 的本质:行级数据访问
DAO(Data Access Object)解决的是"如何访问数据库"的问题。封装的是 SQL 拼接、连接管理、ORM 映射——这些都是技术细节。
java public interface OrderDao { Order findById(Long id); List<Order> findByUserId(Long userId); int insert(Order order); int updateStatus(Long id, String status); int delete(Long id); }
DAO 的粒度是"行"——每次操作一条记录或一组记录。它不知道业务上有"未支付订单"这个概念,只知道数据库里 status = 'PENDING' 的行。
这个抽象层级在小型项目里够用。但当业务复杂起来,DAO 就开始被迫承担不该承担的职责。
DAO 越界的三个信号
第一个信号:DAO 里开始出现业务判断。
java public List<Order> findPendingPaymentOrders(Long userId) { // DAO 里出现了"待支付"这个业务概念 return orderDao.findByUserIdAndStatus(userId, "PENDING_PAYMENT"); }
第二个信号:Service 层把业务逻辑下沉到 DAO 注释里。
java public class OrderService { public void cancelOrder(Long orderId) { // 业务规则:要先把订单状态改成 CANCELLED,再调用支付撤销 // 然后通知库存系统释放库存 // 如果订单已发货则不能取消 // 这些规则放在 Service 层是合理的,但当规则多了 // 就会被搬进 DAO 注释,因为 DAO 知道所有状态 ... } }
第三个信号:DAO 方法签名开始包含业务对象。
```java public interface OrderDao { // DAO 直接接收业务对象做参数 void cancelOrder(Order order, String reason);
// DAO 直接返回业务视图 List<OrderStatisticsView> getOrderStatistics(DateRange range);} ```
这三个信号任意出现两个,就说明你的 DAO 已经越界了。该让 Repository 出场了。
Repository 的本质:集合级领域抽象
Repository 解决的是"如何管理领域对象的集合"的问题。封装的是"这个聚合根有哪些查询、哪些修改、修改的语义是什么"。
```java public interface OrderRepository { // 按 ID 找(唯一标识) Optional findById(OrderId id);
// 业务概念的查询,不是数据库字段 List<Order> findPendingPaymentOrdersFor(CustomerId customerId); List<Order> findOrdersEligibleForRefund(); // 业务动作,不是 CRUD void save(Order order); // 不区分 insert/update void remove(Order order); // 业务上取消订单,底层是软删还是硬删由实现决定} ```
Repository 和 DAO 的差异有三条。
第一条:抽象对象不同。
DAO 的抽象对象是"数据库表"。Repository 的抽象对象是"领域对象集合"。
DAO 方法findByUserIdAndStatus(Long, String)是从数据库角度提问——按用户 ID 和状态过滤。
Repository 方法findPendingPaymentOrdersFor(CustomerId)是从业务角度提问——找这个客户的所有待支付订单。
底层 SQL 怎么写、JOIN 几张表,Repository 不知道也不关心——这是实现细节。
第二条:方法粒度不同。
DAO 的方法是细粒度的——每种过滤条件一个方法。
Repository 的方法是粗粒度的——业务概念对应一个方法。findPendingPaymentOrdersFor内部可以包含 5 种过滤条件、3 个 JOIN,由 Repository 实现去组装。
```java public class JpaOrderRepository implements OrderRepository { private EntityManager em;
public List<Order> findPendingPaymentOrdersFor(CustomerId customerId) { // Repository 实现层依然可以用 JPA/Hibernate/MyBatis // 但调用方不需要知道 return em.createQuery( "SELECT o FROM Order o WHERE o.customerId = :cid " + "AND o.status = 'PENDING_PAYMENT' " + "AND o.createdAt > :threshold", Order.class) .setParameter("cid", customerId) .setParameter("threshold", LocalDateTime.now().minusHours(24)) .getResultList(); }} ```
第三条:业务边界不同。
DAO 不知道业务存在。Repository 知道业务概念——PendingPaymentOrders是一个业务术语,EligibleForRefund是另一个。
这就是 Repository 模式的"抽象层级"差异:DAO 在数据层工作,Repository 在领域层工作。
三个工程化坑
Repository 模式听起来简单,但落地有三个常见的坑。
坑一:把 Repository 退化成 DAO。
这是最常见的错误。名字叫 Repository,方法签名跟 DAO 一模一样。
java // 伪 Repository:还是 DAO 的方法签名 public interface OrderRepository { Optional<Order> findById(Long id); List<Order> findByUserIdAndStatus(Long userId, String status); List<Order> findByCreatedAtBetween(Date start, Date end); }
这种 Repository 没有任何价值,就是给 DAO 套了个新名字。Repository 的方法名应该是业务术语,不是数据库字段名。
判断标准:你的 Repository 方法名能不能念给产品经理听?如果产品经理听不懂,这个方法名就是 DAO 风格的,不是 Repository 风格的。
坑二:把 Repository 退化为通用工具类。
java public interface GenericRepository<T> { T findById(Long id); List<T> findByCondition(Map<String, Object> conditions); void save(T entity); }
这种"通用 Repository"看起来优雅,实际上把业务抽象完全丢了。调用方拿到的是 Map 条件,类型安全、IDE 提示、编译期检查全部失效。
Repository 应该是具体聚合根的,不是泛型的。每个聚合根有自己的 Repository 接口。
坑三:在贫血模型里强行用 Repository。
Repository 模式适合"充血模型"——聚合根有自己的业务行为(order.cancel()、order.markAsPaid())。Repository 负责持久化,业务行为由聚合根自己负责。
如果你的项目是贫血模型——Order 只是个 getter/setter 容器,所有业务逻辑在 Service 层——那 Repository 模式对你没用,因为你没有"业务行为"可以让聚合根承担。
在贫血模型里强行用 Repository,结果就是 Repository 又退化成 DAO(只做 CRUD),Service 层继续承担所有业务逻辑。两层都没变好,徒增复杂度。
真实重构对比
我重构的那个老项目原本是这样的:
java // 老的 OrderDao public interface OrderDao { Order findById(Long id); List<Order> findByUserId(Long userId); int updateStatus(Long id, String status); List<Order> findByStatusAndCreatedAtBefore(String status, Date time); int updatePaymentInfo(Long id, String paymentId, Date paidAt); int cancelOrder(Long id, String reason); // ... 50 多个方法 }
50 多个方法分布在 5 个 Service 里,每个 Service 调用 6-10 个 DAO 方法完成业务逻辑。改一个业务规则要在 5 个 Service、10 个 DAO 方法、20 个调用方里同步修改。
重构后:
java // 新的 OrderRepository public interface OrderRepository { Optional<Order> findById(OrderId id); List<Order> findPendingPaymentOrdersFor(CustomerId customerId); List<Order> findOverdueOrdersFor(CollectionAgencyId agencyId); void save(Order order); void remove(Order order); }
Service 层从 5 个变成 1 个核心 OrderService,所有业务逻辑都在 Order 聚合根内部完成:
```java public class Order { public void cancel(String reason) { if (this.status != OrderStatus.PAID && this.status != OrderStatus.PENDING_PAYMENT) { throw new OrderCannotBeCancelledException(this.id, this.status); } this.status = OrderStatus.CANCELLED; this.cancellationReason = reason; this.cancelledAt = LocalDateTime.now(); // 业务规则都在这里 }
public void markAsPaid(PaymentInfo payment) { if (this.status != OrderStatus.PENDING_PAYMENT) { throw new OrderNotAwaitingPaymentException(this.id, this.status); } this.status = OrderStatus.PAID; this.payment = payment; this.paidAt = LocalDateTime.now(); }} ```
Service 层只剩下用例编排——orderService.cancel(orderId, reason)里面调order.cancel(reason)然后调orderRepository.save(order)。
改业务规则只改 Order 聚合根一个文件。SQL 优化只改 Repository 实现一个文件。互不干扰。
何时用 DAO、何时用 Repository
简单判断标准:
- 如果你的项目是贫血模型(实体只有 getter/setter),用 DAO 就行,Repository 反而是负担
- 如果你的项目是充血模型(有业务行为的聚合根),用 Repository
- 如果你的项目是中型规模、领域概念不多、查询模式简单,用 DAO + Service 足够
- 如果你的项目是大型规模、多个聚合根、复杂查询、用 DDD 划分了边界,用 Repository
不要为了"用设计模式"而用 Repository。Repository 解决的是领域抽象和数据访问的关注点分离问题——如果你没这个问题,DAO 就够了。
DAO 和 Repository 的本质差异是抽象层级:DAO 解决"如何访问数据库",Repository 解决"如何管理领域对象集合"。把这两个混在一起,DAO 就会膨胀成万能类,业务逻辑和数据访问纠缠不清。
说到底,写代码不一定要用 Repository,但写代码一定要分清楚"我现在在解决数据访问问题"还是"我在解决业务问题"。分清楚之后,叫 DAO 还是叫 Repository 都是次要的。