仓库模式(Repository):仓库接口、具体仓库、聚合根,以及DDD中的仓储设计

仓库模式,说白了就是给领域对象找个「管家」。

我在做微服务拆分时,最头疼的就是数据访问逻辑散落一地。业务代码里到处是SQL,改个表结构得翻遍整个项目。后来接触了DDD里的Repository模式,才意识到——原来数据访问也可以这么优雅。

为什么需要仓库模式?

你想想看,传统开发中我们是怎么做的?Service里直接调用DAO,DAO里写SQL,查询结果拼成POJO返回。这有什么问题?

  • 领域逻辑泄露:查询条件、聚合逻辑散落在各处
  • 测试困难:要测业务逻辑,得先搭数据库环境
  • 技术耦合:换数据库?改ORM?得动大量代码

仓库模式就是来解决这些问题的。它把数据访问抽象成接口,业务层只跟接口打交道,底层实现随便换。

核心思想:仓库是领域对象的「集合」——你从仓库拿对象、放回对象,至于对象存在哪、怎么存,仓库自己搞定。

仓库模式的三要素

1. 仓库接口

定义数据访问的契约。我习惯把接口放在领域层,这样领域层就不依赖基础设施了。

// Java 示例
public interface OrderRepository {
    Order findById(OrderId id);
    void save(Order order);
    void delete(OrderId id);
    List<Order> findByCustomerId(CustomerId customerId);
}
// C++ 示例
class OrderRepository {
public:
    virtual ~OrderRepository() = default;
    virtual std::shared_ptr<Order> findById(const OrderId& id) = 0;
    virtual void save(const Order& order) = 0;
    virtual void deleteById(const OrderId& id) = 0;
    virtual std::vector<std::shared_ptr<Order>> findByCustomerId(const CustomerId& customerId) = 0;
};

2. 具体仓库

接口的实现,负责真正的数据存取。可以是JPA、MyBatis、甚至文件系统。

// Java 实现(JPA)
@Repository
public class JpaOrderRepository implements OrderRepository {
    @PersistenceContext
    private EntityManager em;

    @Override
    public Order findById(OrderId id) {
        return em.find(Order.class, id);
    }

    @Override
    public void save(Order order) {
        em.persist(order);
    }
    // ...
}
// C++ 实现(SQLite)
class SqliteOrderRepository : public OrderRepository {
    sqlite3* db;
public:
    std::shared_ptr<Order> findById(const OrderId& id) override {
        // 执行SQL查询,组装Order对象
    }
    void save(const Order& order) override {
        // 插入或更新
    }
};

3. 聚合根

这是DDD里的关键概念。聚合根是领域对象的「根实体」,仓库只对聚合根开放。

举个例子:订单Order是聚合根,它包含OrderItem列表。你不能直接操作OrderItem的仓库——因为OrderItem的生命周期由Order管理。

我的经验:每个聚合根对应一个仓库。如果一个实体没有自己的仓库,那它大概率不是聚合根。

DDD中的仓储设计原则

我在项目中踩过不少坑,总结了几条铁律:

  1. 仓库只返回聚合根——别让仓库返回VO或DTO,那是应用层的事
  2. 仓库方法名要体现领域语义——别叫queryByCondition,叫findActiveOrdersByCustomer
  3. 仓库不处理事务——事务是应用层的职责,仓库只管读写
  4. 仓库不包含业务逻辑——判断、计算、校验都放在领域层

我曾经犯过的错:在仓库里写了大量统计逻辑,结果换数据库时全得重写。后来我把统计逻辑抽到了领域服务里,仓库只做最基础的CRUD。

仓库模式的变体与扩展

变体 适用场景 说明
泛型仓库 基础CRUD操作 减少重复代码,但可能丢失领域语义
规约模式 复杂查询条件 把查询条件封装成对象,仓库根据规约生成查询
缓存仓库 高频读取场景 在仓库内部加缓存,对业务层透明
事件溯源仓库 需要审计/回溯 不存当前状态,存事件流,通过事件还原状态

实战:订单系统的仓库设计

来看一个完整的例子。假设我们要设计订单系统:

// 聚合根:Order
public class Order {
    private OrderId id;
    private CustomerId customerId;
    private List<OrderItem> items;
    private OrderStatus status;
    private Money totalAmount;

    // 领域方法
    public void addItem(Product product, int quantity) {
        // 业务逻辑:检查库存、计算金额
    }

    public void submit() {
        // 业务逻辑:校验、变更状态
    }
}

// 仓库接口
public interface OrderRepository {
    Order findById(OrderId id);
    void save(Order order);
    List<Order> findPendingOrders();
}

// 具体实现
@Repository
public class OrderRepositoryImpl implements OrderRepository {
    @Autowired
    private OrderJpaDao orderJpaDao;
    @Autowired
    private OrderItemJpaDao itemJpaDao;

    @Override
    public Order findById(OrderId id) {
        OrderPO po = orderJpaDao.findById(id.getValue())
            .orElseThrow(() -> new OrderNotFoundException(id));
        List<OrderItemPO> itemPOs = itemJpaDao.findByOrderId(id.getValue());
        return OrderAssembler.toDomain(po, itemPOs);
    }

    @Override
    public void save(Order order) {
        OrderPO po = OrderAssembler.toPO(order);
        orderJpaDao.save(po);
        // 级联保存OrderItem
    }
}

注意:仓库里用到了Assembler(装配器)来做PO和领域对象的转换。这是DDD推荐的实践——让领域对象保持纯净,不沾染任何持久化注解。

仓库模式的核心流程图

仓库模式核心架构 应用层 (Application) 领域层 (Domain) 基础设施层 (Infrastructure) 调用 依赖 聚合根 (Aggregate Root) 仓库接口 (Repository Interface) 具体仓库实现 ORM / DAO / 数据库 实现 关键约束: • 应用层只依赖领域层(仓库接口),不依赖基础设施层 • 领域层定义仓库接口,不关心具体实现 • 基础设施层实现仓库接口,对领域层透明 • 依赖倒置:高层模块不依赖低层模块,双方都依赖抽象

避坑指南

我这些年用仓库模式,踩过的坑不少。分享几个典型的:

  • 别把仓库当DAO用——DAO是数据访问对象,返回PO;仓库返回领域对象。混用会导致领域层被持久化细节污染
  • 别在仓库里做跨聚合的操作——比如在OrderRepository里同时保存User。跨聚合的操作应该由领域服务或应用层协调
  • 别滥用泛型仓库——泛型仓库虽然省事,但容易丢失领域语义。我一般只对基础CRUD用泛型,复杂查询还是单独写
  • 注意事务边界——仓库不管理事务,但多个仓库操作需要事务时,要在应用层统一控制

我的习惯:每个聚合根一个仓库接口,一个实现类。如果项目简单,可以用泛型仓库减少代码量。但一旦业务复杂起来,还是老老实实写具体仓库——可读性和可维护性更重要。

总结

仓库模式不是什么高深的技术,它就是一个分层思想的具体体现。把数据访问抽象出来,让业务代码只关心业务逻辑。

说白了,就是让该干数据库的干数据库,该干业务的干业务。别混在一起,否则改起来真要命。

嗯,如果你正在做DDD落地,仓库模式是绕不开的一环。从最简单的接口+实现开始,慢慢你就会发现——代码清爽多了。

公众号:蓝海资料掘金营,微信deep3321