在领域驱动设计(DDD,Domain-Driven Design)中, 仓储(Repository) 是一个重要的概念,用于解决持久化和领域模型之间的隔离问题。简单来说,仓储是领域模型与持久化存储(如数据库)之间的桥梁,它提供了一种在应用程序中操作领域对象的方式,而无需直接与数据库或底层存储交互。

以下是对仓储的详细解释:
仓储的定义
仓储(Repository)的核心思想是:
提供一个中间层,让应用程序可以像操作内存中的对象集合一样操作领域对象,而不关心对象的持久化实现细节。
仓储是一个领域对象集合的抽象,应用程序可以通过它来存储、查询、更新、删除领域对象。
仓储的设计原则
-
隐藏持久化细节:
仓储屏蔽了底层存储的复杂性(如 SQL 查询、事务管理等),领域层只需要通过仓储接口与之交互,而无需关心存储的实现细节。 -
提供领域模型的聚合操作:
仓储通常与聚合根(Aggregate Root)紧密相关。每个聚合(Aggregate)通常会有一个对应的仓储,仓储只负责管理聚合根及其聚合内的对象。 -
保持统一接口:
仓储通过接口定义了对领域对象的基本操作,比如添加、删除、查找和更新等操作。
仓储的职责
-
存储领域对象:
负责将领域对象保存到持久化存储(如数据库)。 -
获取领域对象:
提供方法来根据条件查询领域对象(如通过 ID 查找订单)。 -
删除领域对象:
负责从存储中移除领域对象。 -
隐藏存储细节:
将底层存储逻辑封装起来(如 SQL 查询或 ORM 操作),让领域模型专注于业务逻辑。
仓储的实现方法
以下是使用 Java 的仓储模式实现的代码示例,包括详细注释,生产环境使用需要根据需求来调整。
示例场景:订单仓储
1. 定义聚合根(领域对象)
import java.time.LocalDateTime;
/**
* 订单(Order):这是领域模型中的聚合根。
* 聚合根是领域模型中的一个重要概念,表示领域的一部分及其边界。
*/
public class Order {
private final String orderId; // 订单的唯一标识
private final String userId; // 用户 ID
private double totalAmount; // 订单总金额
private LocalDateTime createdAt; // 订单创建时间
public Order(String orderId, String userId, double totalAmount) {
this.orderId = orderId;
this.userId = userId;
this.totalAmount = totalAmount;
this.createdAt = LocalDateTime.now();
}
// Getter 和 Setter 方法
public String getOrderId() {
return orderId;
}
public String getUserId() {
return userId;
}
public double getTotalAmount() {
return totalAmount;
}
public void setTotalAmount(double totalAmount) {
this.totalAmount = totalAmount;
}
public LocalDateTime getCreatedAt() {
return createdAt;
}
}
2. 定义仓储接口
import java.util.Optional;
/**
* 订单仓储接口(Order Repository)。
* 定义了对订单领域对象的基本操作。
*/
public interface OrderRepository {
/**
* 保存订单。
*
* @param order 要保存的订单对象
*/
void save(Order order);
/**
* 根据订单 ID 查找订单。
*
* @param orderId 订单 ID
* @return 返回订单对象的 Optional(可能存在,也可能不存在)
*/
Optional<Order> findById(String orderId);
/**
* 删除订单。
*
* @param orderId 订单 ID
*/
void delete(String orderId);
}
3. 仓储实现(基于内存的示例)
在学习中,仓储通常通过数据库实现(如基于 JDBC、JPA、mybatis等)。这里我们先用内存实现,便于理解:
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
/**
* 订单仓储的内存实现。
* 模拟将订单存储在内存中。
*/
public class InMemoryOrderRepository implements OrderRepository {
private final Map<String, Order> storage = new HashMap<>(); // 用于模拟数据库的存储
@Override
public void save(Order order) {
storage.put(order.getOrderId(), order); // 将订单保存到内存中
System.out.println("订单已保存,订单 ID: " + order.getOrderId());
}
@Override
public Optional<Order> findById(String orderId) {
// 从内存中查找订单
return Optional.ofNullable(storage.get(orderId));
}
@Override
public void delete(String orderId) {
storage.remove(orderId); // 从内存中移除订单
System.out.println("订单已删除,订单 ID: " + orderId);
}
}
4. 测试代码
public class RepositoryExample {
public static void main(String[] args) {
// 创建一个仓储实例
OrderRepository orderRepository = new InMemoryOrderRepository();
// 创建订单
Order order = new Order("12345", "user001", 299.99);
// 保存订单
orderRepository.save(order);
// 查找订单
orderRepository.findById("12345").ifPresentOrElse(
foundOrder -> System.out.println("找到订单,用户 ID: " + foundOrder.getUserId() + ", 总金额: " + foundOrder.getTotalAmount()),
() -> System.out.println("订单未找到!")
);
// 删除订单
orderRepository.delete("12345");
// 再次查找订单
orderRepository.findById("12345").ifPresentOrElse(
foundOrder -> System.out.println("找到订单,用户 ID: " + foundOrder.getUserId()),
() -> System.out.println("订单未找到!")
);
}
}
运行结果
订单已保存,订单 ID: 12345
找到订单,用户 ID: user001, 总金额: 299.99
订单已删除,订单 ID: 12345
订单未找到!
仓储的核心原理
-
隐藏存储细节:
- 仓储封装了底层存储的逻辑(如保存、查找、删除等),使得领域模型不需要直接依赖数据库操作。
-
像操作集合一样操作领域对象:
- 仓储让我们能够像操作内存中的集合一样操作领域对象,而底层实现可能使用数据库、文件存储等方式。
-
与聚合根紧密关联:
- 仓储只负责管理聚合根,聚合根内部的其他对象由聚合根本身管理。例如,
Order
是聚合根,仓储只操作Order
,而不会直接操作订单中的明细条目。
- 仓储只负责管理聚合根,聚合根内部的其他对象由聚合根本身管理。例如,
-
实现领域逻辑与基础设施的解耦:
- 通过接口定义仓储,领域层只依赖仓储接口,具体的实现由基础设施层提供。这种设计符合 DDD 的分层架构。
仓储在生产环境中的使用
在生产环境中,仓储通常使用 ORM 工具(如 mybatis、JPA)或直接使用数据库查询实现。以上的内存实现是一个简单的演示,实际环境中可以通过数据库实现。
如下是基于 JPA 的仓储实现示例:
@Repository
public class JpaOrderRepository implements OrderRepository {
@PersistenceContext
private EntityManager entityManager;
@Override
public void save(Order order) {
entityManager.persist(order);
}
@Override
public Optional<Order> findById(String orderId) {
return Optional.ofNullable(entityManager.find(Order.class, orderId));
}
@Override
public void delete(String orderId) {
Order order = entityManager.find(Order.class, orderId);
if (order != null) {
entityManager.remove(order);
}
}
}
基于 MyBatis 的仓储实现示例
场景:订单(Order)仓储
- 需求:
- 需要对订单进行基本的持久化操作,包括保存、查询和删除。
- 使用 MyBatis 进行 SQL 操作。
1. 定义领域模型
import java.time.LocalDateTime;
/**
* 订单(Order):这是领域模型中的聚合根。
* 聚合根是领域模型中的一个重要概念,表示领域的一部分及其边界。
*/
public class Order {
private String orderId; // 订单的唯一标识
private String userId; // 用户 ID
private double totalAmount; // 订单总金额
private LocalDateTime createdAt; // 订单创建时间
// 构造方法
public Order(String orderId, String userId, double totalAmount) {
this.orderId = orderId;
this.userId = userId;
this.totalAmount = totalAmount;
this.createdAt = LocalDateTime.now();
}
// Getter 和 Setter 方法
public String getOrderId() {
return orderId;
}
public void setOrderId(String orderId) {
this.orderId = orderId;
}
public String getUserId() {
return userId;
}
public void setUserId(String userId) {
this.userId = userId;
}
public double getTotalAmount() {
return totalAmount;
}
public void setTotalAmount(double totalAmount) {
this.totalAmount = totalAmount;
}
public LocalDateTime getCreatedAt() {
return createdAt;
}
public void setCreatedAt(LocalDateTime createdAt) {
this.createdAt = createdAt;
}
}
2. 定义仓储接口
仓储接口定义了操作领域模型的基本方法。
import java.util.Optional;
/**
* 订单仓储接口(OrderRepository)。
* 定义对订单领域对象的持久化操作。
*/
public interface OrderRepository {
/**
* 保存订单。
*
* @param order 要保存的订单对象
*/
void save(Order order);
/**
* 根据订单 ID 查找订单。
*
* @param orderId 订单 ID
* @return 返回订单对象的 Optional(可能存在,也可能不存在)
*/
Optional<Order> findById(String orderId);
/**
* 删除订单。
*
* @param orderId 订单 ID
*/
void delete(String orderId);
}
3. 创建 MyBatis Mapper XML 文件
在 MyBatis 中,SQL 语句通常定义在 Mapper 文件中。以下是一个基于 MyBatis 的 OrderMapper.xml
文件示例:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.repository.OrderMapper">
<!-- 保存订单 -->
<insert id="saveOrder">
INSERT INTO orders (order_id, user_id, total_amount, created_at)
VALUES (#{orderId}, #{userId}, #{totalAmount}, #{createdAt})
</insert>
<!-- 根据订单 ID 查询订单 -->
<select id="findOrderById" resultType="com.example.domain.Order">
SELECT order_id AS orderId,
user_id AS userId,
total_amount AS totalAmount,
created_at AS createdAt
FROM orders
WHERE order_id = #{orderId}
</select>
<!-- 删除订单 -->
<delete id="deleteOrder">
DELETE FROM orders WHERE order_id = #{orderId}
</delete>
</mapper>
- 文件解析:
namespace
指定了 Mapper 的全限定名。- 每个
<insert>
、<select>
和<delete>
标签分别对应保存、查询和删除操作。 - 使用
#{}
来映射 Java 对象的属性到 SQL 参数。
4. 实现仓储接口
通过 MyBatis 的 Mapper 动态代理实现仓储接口。
import org.apache.ibatis.annotations.Mapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;
import java.util.Optional;
/**
* 基于 MyBatis 的订单仓储实现。
*/
@Repository
public class MyBatisOrderRepository implements OrderRepository {
private final OrderMapper orderMapper; // 注入 MyBatis Mapper
@Autowired
public MyBatisOrderRepository(OrderMapper orderMapper) {
this.orderMapper = orderMapper;
}
@Override
public void save(Order order) {
orderMapper.saveOrder(order); // 调用 MyBatis Mapper 的 save 方法
}
@Override
public Optional<Order> findById(String orderId) {
return Optional.ofNullable(orderMapper.findOrderById(orderId));
}
@Override
public void delete(String orderId) {
orderMapper.deleteOrder(orderId); // 调用 MyBatis Mapper 的 delete 方法
}
}
5. 定义 MyBatis Mapper 接口
MyBatis 会为 Mapper 接口生成动态代理,实现具体的 SQL 调用。
@Mapper
public interface OrderMapper {
void saveOrder(Order order); // 对应 XML 文件中的 saveOrder
Order findOrderById(String orderId); // 对应 XML 文件中的 findOrderById
void deleteOrder(String orderId); // 对应 XML 文件中的 deleteOrder
}
6. 数据库表结构
确保有一个对应的数据库表 orders
,结构如下:
CREATE TABLE orders (
order_id VARCHAR(50) PRIMARY KEY,
user_id VARCHAR(50) NOT NULL,
total_amount DECIMAL(10, 2) NOT NULL,
created_at TIMESTAMP NOT NULL
);
7. 测试代码
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;
/**
* 仓储测试类。
* 演示如何使用基于 MyBatis 的仓储实现。
*/
@Component
public class RepositoryTest implements CommandLineRunner {
private final OrderRepository orderRepository;
@Autowired
public RepositoryTest(OrderRepository orderRepository) {
this.orderRepository = orderRepository;
}
@Override
public void run(String... args) {
// 创建一个订单
Order order = new Order("12345", "user001", 299.99);
// 保存订单
orderRepository.save(order);
System.out.println("订单已保存!");
// 查找订单
orderRepository.findById("12345").ifPresentOrElse(
foundOrder -> System.out.println("找到订单,用户 ID: " + foundOrder.getUserId() + ", 总金额: " + foundOrder.getTotalAmount()),
() -> System.out.println("订单未找到!")
);
// 删除订单
orderRepository.delete("12345");
System.out.println("订单已删除!");
}
}
8. 运行结果
执行测试代码时,控制台输出如下:
订单已保存!
找到订单,用户 ID: user001, 总金额: 299.99
订单已删除!
小结
基于 MyBatis 的仓储实现将领域模型(Order)与数据库操作解耦,生产上通过以下方式实现代码:
- 定义领域模型:表示业务数据和逻辑。
- 定义仓储接口:提供统一的领域对象操作方法。
- 使用 MyBatis Mapper:通过 XML 或注解定义 SQL,完成数据库交互。
- 仓储实现:通过 MyBatis 的 Mapper 动态代理实现仓储接口。
这种实现方式符合 DDD 的设计原则,同时充分利用 MyBatis 强大的 SQL 映射能力,是生产环境中一种高效且可靠的实现方式。
总结
在 DDD 设计原则中,仓储的作用是为领域模型提供对持久化存储的访问,同时隐藏存储细节,从而实现领域逻辑与基础设施的解耦。通过仓储模式,我们能够更方便地操作领域对象,同时保持代码的清晰性和可扩展性。