仓库模式(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中的仓储设计原则
我在项目中踩过不少坑,总结了几条铁律:
- 仓库只返回聚合根——别让仓库返回VO或DTO,那是应用层的事
- 仓库方法名要体现领域语义——别叫queryByCondition,叫findActiveOrdersByCustomer
- 仓库不处理事务——事务是应用层的职责,仓库只管读写
- 仓库不包含业务逻辑——判断、计算、校验都放在领域层
我曾经犯过的错:在仓库里写了大量统计逻辑,结果换数据库时全得重写。后来我把统计逻辑抽到了领域服务里,仓库只做最基础的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推荐的实践——让领域对象保持纯净,不沾染任何持久化注解。
仓库模式的核心流程图
避坑指南
我这些年用仓库模式,踩过的坑不少。分享几个典型的:
- 别把仓库当DAO用——DAO是数据访问对象,返回PO;仓库返回领域对象。混用会导致领域层被持久化细节污染
- 别在仓库里做跨聚合的操作——比如在OrderRepository里同时保存User。跨聚合的操作应该由领域服务或应用层协调
- 别滥用泛型仓库——泛型仓库虽然省事,但容易丢失领域语义。我一般只对基础CRUD用泛型,复杂查询还是单独写
- 注意事务边界——仓库不管理事务,但多个仓库操作需要事务时,要在应用层统一控制
我的习惯:每个聚合根一个仓库接口,一个实现类。如果项目简单,可以用泛型仓库减少代码量。但一旦业务复杂起来,还是老老实实写具体仓库——可读性和可维护性更重要。
总结
仓库模式不是什么高深的技术,它就是一个分层思想的具体体现。把数据访问抽象出来,让业务代码只关心业务逻辑。
说白了,就是让该干数据库的干数据库,该干业务的干业务。别混在一起,否则改起来真要命。
嗯,如果你正在做DDD落地,仓库模式是绕不开的一环。从最简单的接口+实现开始,慢慢你就会发现——代码清爽多了。