ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

你写的 DAO 越来越像万能类——Repository 模式跟 DAO 差了一个抽象层

你写的 DAO 越来越像万能类——Repository 模式跟 DAO 差了一个抽象层 你写的 DAO 越来越像万能类——Repository 模式跟 DAO 差了一个抽象层Java 后端项目里DAO 和 Repository 这两个词经常被混着用甚至很多团队里就是同一个东西两个名字。但我最近重构一个 3 年老项目时发现这俩在抽象层级上完全不是一回事。当你把所有数据访问都叫 DAO 时你的 DAO 就会越写越大最后变成什么都能查、什么都能改的万能类业务逻辑和数据访问混在一起改一行 SQL 动 20 个文件。Repository 模式不是给 DAO 换名字是把数据访问的抽象层级从行升到集合。DAO 的本质行级数据访问DAOData Access Object解决的是如何访问数据库的问题。封装的是 SQL 拼接、连接管理、ORM 映射——这些都是技术细节。java public interface OrderDao { Order findById(Long id); ListOrder 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 ListOrder 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 直接返回业务视图 ListOrderStatisticsView getOrderStatistics(DateRange range);} 这三个信号任意出现两个就说明你的 DAO 已经越界了。该让 Repository 出场了。Repository 的本质集合级领域抽象Repository 解决的是如何管理领域对象的集合的问题。封装的是这个聚合根有哪些查询、哪些修改、修改的语义是什么。java public interface OrderRepository { // 按 ID 找唯一标识 Optional findById(OrderId id);// 业务概念的查询不是数据库字段 ListOrder findPendingPaymentOrdersFor(CustomerId customerId); ListOrder 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 ListOrder 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 { OptionalOrder findById(Long id); ListOrder findByUserIdAndStatus(Long userId, String status); ListOrder findByCreatedAtBetween(Date start, Date end); }这种 Repository 没有任何价值就是给 DAO 套了个新名字。Repository 的方法名应该是业务术语不是数据库字段名。判断标准你的 Repository 方法名能不能念给产品经理听如果产品经理听不懂这个方法名就是 DAO 风格的不是 Repository 风格的。坑二把 Repository 退化为通用工具类。java public interface GenericRepositoryT { T findById(Long id); ListT findByCondition(MapString, 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只做 CRUDService 层继续承担所有业务逻辑。两层都没变好徒增复杂度。真实重构对比我重构的那个老项目原本是这样的java // 老的 OrderDao public interface OrderDao { Order findById(Long id); ListOrder findByUserId(Long userId); int updateStatus(Long id, String status); ListOrder 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 { OptionalOrder findById(OrderId id); ListOrder findPendingPaymentOrdersFor(CustomerId customerId); ListOrder 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 都是次要的。
返回列表