第8章 仓储 - 聚合的持久化 (Repositories - Persistence for Aggregates)
一、导读
8.1 本章学习目标
通过本章的学习,读者将能够:
理解仓储(Repository)在 DDD 中的定义与核心职责
掌握仓储与 DAO、ORM 的区别
学会设计仓储接口,保持领域层的独立性
理解仓储与聚合的关系
掌握仓储的实现模式,包括内存实现和数据库实现
能够在实际项目中正确设计和实现仓储
理解仓储与查询的分离(CQRS 思想)
掌握处理复杂查询的最佳实践
8.2 为什么本章如此重要
仓储是 DDD 中连接领域层和基础设施层的桥梁。它提供了一种机制,让领域对象可以像集合一样被访问,而无需关心底层的持久化细节。这种抽象使得领域层可以专注于业务逻辑,而不被技术实现所污染。
在传统开发中,数据访问逻辑往往散落在业务的各个地方。SQL 语句、ORM 调用、缓存操作等技术细节与业务逻辑混杂在一起,导致代码难以理解和维护。仓储模式通过将持久化逻辑集中到专门的组件中,实现了关注点分离。
仓储的重要性还体现在它对领域模型的保护上。如果没有仓储抽象,领域对象可能需要实现持久化相关的接口,或者暴露内部状态以供 ORM 框架使用。这些都会破坏领域模型的纯净性。通过仓储,领域对象可以保持对持久化的无知,专注于表达业务。
此外,仓储为测试提供了便利。通过定义仓储接口,可以在单元测试中使用内存实现或 mock 对象,无需依赖真实的数据库。这大大提高了测试的速度和可靠性。
在现代架构中,仓储还与 CQRS 模式密切相关。传统的仓储同时负责命令(写)和查询(读),但在复杂系统中,读写的需求差异很大。CQRS 将读写分离,写操作通过仓储保证一致性,读操作通过专门的查询服务提供高效的数据访问。
8.3 核心问题引导
在学习本章之前,请思考以下问题:
什么是仓储?它与普通的 DAO 有什么区别?
仓储应该放在哪一层?领域层还是基础设施层?
仓储接口和实现应该如何分离?
仓储是否应该支持复杂查询?还是只负责按 ID 获取?
仓储与聚合是什么关系?是否每个聚合都需要一个仓储?
如何处理仓储的更新操作?是全量更新还是增量更新?
仓储是否应该管理事务?还是由应用层管理?
在微服务架构中,仓储的设计有什么特殊考虑?
二、核心概念详解
8.1 仓储的定义与本质
仓储(Repository)是 DDD 中用于管理聚合持久化的模式。它提供了一个类似集合的接口,让客户端可以像操作内存集合一样操作领域对象,而无需关心底层的存储机制。
Eric Evans 在《领域驱动设计》中将仓储定义为:"一种封装了存储、查询和遍历行为的机制,行为类似于集合但更强大。"
仓储的本质在于它是一个抽象,它将持久化的复杂性隐藏起来,为领域层提供一个简单的接口。通过这个接口,领域对象可以被保存、检索和删除,而领域层不需要知道数据是如何存储的。
仓储具有以下核心特征:
面向聚合
仓储的操作单位是聚合,而不是单个实体或表。仓储保存和检索整个聚合,确保聚合的一致性边界得到尊重。
集合语义
仓储提供了类似集合的操作:添加、删除、查询。但这些操作背后是复杂的持久化逻辑,如数据库插入、更新、删除等。
抽象性
仓储是一个抽象概念,通常以接口的形式存在于领域层。具体的实现(如基于数据库的实现)位于基础设施层。
双向性
仓储既可以将领域对象持久化到存储中,也可以从存储中重建领域对象。它是领域对象和持久化数据之间的转换器。
8.2 仓储与 DAO 的区别
仓储和 DAO(Data Access Object)都是用于数据访问的模式,但它们有本质的区别:
| 特征 | 仓储 | DAO |
|---|---|---|
| 抽象层次 | 领域层,面向业务 | 基础设施层,面向数据 |
| 操作单位 | 聚合 | 表/记录 |
| 语言 | 通用语言,业务术语 | 技术术语,SQL/ORM |
| 职责 | 聚合的持久化和检索 | 数据的 CRUD |
| 依赖方向 | 领域层定义接口,基础设施层实现 | 直接依赖数据库 |
DAO 通常是表级别的抽象,提供对数据库表的 CRUD 操作。DAO 的方法名往往反映技术操作,如 insert()、update()、findByPrimaryKey()。
仓储是领域级别的抽象,提供对聚合的保存和检索。仓储的方法名使用业务术语,如 save()、findById()、findByCustomerId()。
另一个关键区别是依赖方向。在 DDD 中,仓储接口定义在领域层,实现在基础设施层。这遵循了依赖倒置原则,使得领域层不依赖于具体的持久化技术。而 DAO 通常直接依赖于数据库,难以替换。
8.3 仓储的职责
仓储的主要职责包括:
保存聚合
将聚合的状态持久化到存储中。这包括新聚合的插入和已有聚合的更新。
仓储应该保存整个聚合,包括聚合根和所有内部对象。如果聚合内部有变化,仓储负责将这些变化同步到存储中。
检索聚合
根据标识或其他条件从存储中重建聚合。重建的聚合应该处于一致的状态,所有内部对象都正确初始化。
仓储返回的是完整的聚合,而不是部分数据。如果需要部分数据,应该使用专门的查询服务。
删除聚合
从存储中移除聚合。删除聚合意味着删除聚合内的所有相关数据。
提供查询方法
提供一些基本的查询方法,如按 ID 查找、按业务键查找等。但仓储不应该承担复杂查询的职责。
需要注意的是,仓储不负责:
- 业务逻辑验证:这是聚合根的职责
- 事务管理:这是应用层的职责
- 复杂报表查询:这是查询服务的职责
- 缓存策略:这是基础设施层的关注点
8.4 仓储接口的设计
仓储接口应该定义在领域层,使用业务语言,不暴露技术细节。一个好的仓储接口应该具备以下特点:
简洁
仓储接口应该尽可能简洁,只提供必要的方法。典型的方法包括:
public interface OrderRepository {
Optional<Order> findById(OrderId id);
void save(Order order);
void delete(OrderId id);
}使用领域类型
仓储方法的参数和返回值应该使用领域类型(实体、值对象),而不是技术类型(ResultSet、DataRow)。
返回 Optional 或 null
对于可能找不到的查询,返回 Optional<T>(Java)或允许返回 None(Python),明确表示可能不存在。
避免泄露持久化细节
仓储接口不应该暴露 SQL、ORM 会话、连接对象等技术细节。
命名清晰
方法名应该清晰表达意图,如 findById、findByCustomerId,而不是 get、query 等模糊名称。
8.5 仓储的实现
仓储的实现位于基础设施层,可以使用各种持久化技术:
关系数据库
使用 JDBC、JPA、MyBatis 等技术实现仓储。需要将领域对象映射到关系表。
文档数据库
使用 MongoDB、CouchDB 等文档数据库实现仓储。文档数据库天然适合存储聚合,因为聚合可以序列化为一个文档。
内存存储
用于测试或临时数据存储。将所有聚合保存在内存中,系统重启后数据丢失。
混合实现
对于复杂场景,可以结合多种存储技术。例如,主要数据存储在关系数据库,缓存使用 Redis。
无论使用哪种技术,仓储的实现都应该对领域层透明。领域层只依赖仓储接口,不关心具体实现。
8.6 仓储与事务
仓储本身不应该管理事务,事务管理是应用层的职责。应用服务开启事务,调用仓储保存聚合,然后提交或回滚事务。
典型的流程如下:
@Transactional
public CreateOrderResult createOrder(CreateOrderRequest request) {
// 创建聚合
Order order = orderFactory.createOrder(request);
// 通过仓储保存
orderRepository.save(order);
// 事务由应用层管理,这里自动提交
return CreateOrderResult.success(order.getId());
}仓储的 save() 方法只是将聚合的状态同步到存储中,实际的提交由事务管理器控制。如果后续操作失败,事务回滚,仓储的保存也会撤销。
8.7 仓储与查询的分离
传统的仓储同时负责命令(写)和查询(读)。但在复杂系统中,读写的需求差异很大:
- 写操作需要保证一致性,加载完整的聚合
- 读操作只需要部分数据,追求性能和灵活性
CQRS(Command Query Responsibility Segregation)模式将读写分离:
- 命令侧使用仓储,保证聚合的一致性
- 查询侧使用专门的查询服务,直接访问数据库或读模型
查询服务的特点:
- 返回 DTO 或投影,不是领域对象
- 支持复杂查询、排序、分页
- 可以针对特定视图优化
- 不修改状态
这种分离使得读写可以独立优化,互不影响。
8.8 仓储的版本控制
在一些场景中,需要处理聚合的并发修改。常见的策略包括:
乐观锁
在聚合中添加版本号字段,每次更新时检查版本号。如果版本号不匹配,说明有其他修改,抛出异常。
public class Order {
private int version;
public void incrementVersion() {
this.version++;
}
}仓储在保存时检查版本号:
public void save(Order order) {
int updated = jdbcTemplate.update(
"UPDATE orders SET ..., version = version + 1 WHERE id = ? AND version = ?",
order.getVersion()
);
if (updated == 0) {
throw new ConcurrentModificationException("Order was modified by another transaction");
}
}悲观锁
在读取时锁定记录,防止其他事务修改。适用于高冲突场景,但会降低并发性能。
事件溯源
不保存当前状态,而是保存一系列事件。通过重放事件重建聚合状态。天然支持版本控制和历史追溯。
三、程序员的视角
8.1 在实际项目中设计仓储
作为程序员,在日常工作中设计仓储时,应该注意以下几个方面:
首先,为每个聚合根定义一个仓储接口。接口放在领域层,使用领域类型,不包含技术细节。
其次,仓储接口应该简洁。只提供必要的方法:保存、按 ID 查找、删除。额外的查询方法应该有明确的业务目的。
第三,仓储的实现放在基础设施层。实现类依赖具体的持久化技术,如 JPA、MyBatis、MongoDB 等。
第四,在单元测试中使用内存实现或 mock 对象。这样可以快速测试业务逻辑,无需启动数据库。
第五,区分仓储和查询服务。仓储负责聚合的持久化,查询服务负责复杂查询。不要试图让仓储承担所有数据访问职责。
第六,考虑使用泛型仓储基类来减少重复代码。但要注意,泛型仓储不应该替代特定业务的仓储接口。
8.2 常见陷阱与误区
在设计和使用仓储时,常见的陷阱包括:
陷阱一:将仓储等同于 DAO。仓储是领域层的抽象,DAO 是基础设施层的技术实现。混淆两者会导致领域层污染。
陷阱二:在仓储中包含业务逻辑。仓储只负责持久化,业务规则应该在聚合或领域服务中。
陷阱三:仓储方法过多。试图在一个仓储中提供所有可能的查询方法,导致接口臃肿。应该保持仓储简洁,复杂查询交给查询服务。
陷阱四:泄露持久化细节。仓储接口暴露 SQL、ORM 会话等技术类型,破坏了抽象。
陷阱五:忽视并发控制。没有考虑多个事务同时修改同一个聚合的情况,导致数据不一致。
陷阱六:仓储管理事务。仓储只负责保存和检索,事务管理应该是应用层的职责。
8.3 与其他模式的协作
仓储与其他 DDD 模式紧密协作:
与聚合协作:仓储的操作单位是聚合。仓储保存和检索整个聚合,确保一致性边界。
与实体协作:仓储通过聚合根访问聚合内部的对象。外部不能直接通过仓储获取聚合内部实体。
与值对象协作:值对象作为聚合的一部分被仓储持久化。仓储负责序列化和反序列化值对象。
与领域服务协作:领域服务可以通过仓储获取聚合,执行业务操作,然后通过仓储保存。
与应用服务协作:应用服务调用仓储保存和检索聚合,管理事务边界。
四、代码实战
8.1 Java 示例:仓储接口与实现
/**
* 订单仓储接口
* 定义在领域层
*/
public interface OrderRepository {
/**
* 根据 ID 查找订单
*/
Optional<Order> findById(OrderId id);
/**
* 根据客户 ID 查找订单
*/
List<Order> findByCustomerId(CustomerId customerId);
/**
* 保存订单(新增或更新)
*/
void save(Order order);
/**
* 删除订单
*/
void delete(OrderId id);
/**
* 查找指定状态的订单
*/
List<Order> findByStatus(OrderStatus status);
}
/**
* 基于 JPA 的订单仓储实现
* 位于基础设施层
*/
@Repository
public class JpaOrderRepository implements OrderRepository {
private final OrderJpaRepository jpaRepository;
private final OrderMapper mapper;
public JpaOrderRepository(OrderJpaRepository jpaRepository, OrderMapper mapper) {
this.jpaRepository = jpaRepository;
this.mapper = mapper;
}
@Override
public Optional<Order> findById(OrderId id) {
return jpaRepository.findById(id.getValue())
.map(mapper::toDomain);
}
@Override
public List<Order> findByCustomerId(CustomerId customerId) {
return jpaRepository.findByCustomerId(customerId.getValue())
.stream()
.map(mapper::toDomain)
.collect(Collectors.toList());
}
@Override
public void save(Order order) {
OrderEntity entity = mapper.toEntity(order);
jpaRepository.save(entity);
}
@Override
public void delete(OrderId id) {
jpaRepository.deleteById(id.getValue());
}
@Override
public List<Order> findByStatus(OrderStatus status) {
return jpaRepository.findByStatus(status.name())
.stream()
.map(mapper::toDomain)
.collect(Collectors.toList());
}
}
/**
* JPA Entity
* 基础设施层的数据模型
*/
@Entity
@Table(name = "orders")
public class OrderEntity {
@Id
private String id;
private String customerId;
private String status;
private BigDecimal totalAmount;
private String currency;
@OneToMany(mappedBy = "order", cascade = CascadeType.ALL, orphanRemoval = true)
private List<OrderItemEntity> items = new ArrayList<>();
@Version
private int version;
// Getters and Setters
public String getId() { return id; }
public void setId(String id) { this.id = id; }
public String getCustomerId() { return customerId; }
public void setCustomerId(String customerId) { this.customerId = customerId; }
public String getStatus() { return status; }
public void setStatus(String status) { this.status = status; }
public BigDecimal getTotalAmount() { return totalAmount; }
public void setTotalAmount(BigDecimal totalAmount) { this.totalAmount = totalAmount; }
public String getCurrency() { return currency; }
public void setCurrency(String currency) { this.currency = currency; }
public List<OrderItemEntity> getItems() { return items; }
public void setItems(List<OrderItemEntity> items) { this.items = items; }
public int getVersion() { return version; }
public void setVersion(int version) { this.version = version; }
}
/**
* 订单项 JPA Entity
*/
@Entity
@Table(name = "order_items")
public class OrderItemEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne
@JoinColumn(name = "order_id")
private OrderEntity order;
private String productId;
private String productName;
private BigDecimal unitPrice;
private int quantity;
// Getters and Setters
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public OrderEntity getOrder() { return order; }
public void setOrder(OrderEntity order) { this.order = order; }
public String getProductId() { return productId; }
public void setProductId(String productId) { this.productId = productId; }
public String getProductName() { return productName; }
public void setProductName(String productName) { this.productName = productName; }
public BigDecimal getUnitPrice() { return unitPrice; }
public void setUnitPrice(BigDecimal unitPrice) { this.unitPrice = unitPrice; }
public int getQuantity() { return quantity; }
public void setQuantity(int quantity) { this.quantity = quantity; }
}
/**
* 领域对象与 Entity 的映射器
*/
@Component
public class OrderMapper {
public Order toDomain(OrderEntity entity) {
OrderId orderId = new OrderId(entity.getId());
CustomerId customerId = new CustomerId(entity.getCustomerId());
Order order = new Order(orderId, customerId);
// 恢复订单项
for (OrderItemEntity itemEntity : entity.getItems()) {
ProductId productId = new ProductId(itemEntity.getProductId());
Money unitPrice = new Money(itemEntity.getUnitPrice(),
Currency.getInstance(entity.getCurrency()));
OrderItem item = new OrderItem(productId, itemEntity.getProductName(),
unitPrice, itemEntity.getQuantity());
// 通过反射或包可见方法添加
order.addItemDirectly(item);
}
// 恢复状态
order.setStatusForRebuild(OrderStatus.valueOf(entity.getStatus()));
return order;
}
public OrderEntity toEntity(Order order) {
OrderEntity entity = new OrderEntity();
entity.setId(order.getId().getValue());
entity.setCustomerId(order.getCustomerId().getValue());
entity.setStatus(order.getStatus().name());
entity.setTotalAmount(order.getTotalAmount().getAmount());
entity.setCurrency(order.getTotalAmount().getCurrency().getCurrencyCode());
entity.setVersion(0); // 版本号由 JPA 管理
// 转换订单项
for (OrderItem item : order.getItems()) {
OrderItemEntity itemEntity = new OrderItemEntity();
itemEntity.setOrder(entity);
itemEntity.setProductId(item.getProductId().getValue());
itemEntity.setProductName(item.getProductName());
itemEntity.setUnitPrice(item.getUnitPrice().getAmount());
itemEntity.setQuantity(item.getQuantity());
entity.getItems().add(itemEntity);
}
return entity;
}
}
/**
* Spring Data JPA Repository
*/
public interface OrderJpaRepository extends JpaRepository<OrderEntity, String> {
List<OrderEntity> findByCustomerId(String customerId);
List<OrderEntity> findByStatus(String status);
}
/**
* 内存仓储实现(用于测试)
*/
public class InMemoryOrderRepository implements OrderRepository {
private final Map<String, Order> store = new ConcurrentHashMap<>();
@Override
public Optional<Order> findById(OrderId id) {
return Optional.ofNullable(store.get(id.getValue()));
}
@Override
public List<Order> findByCustomerId(CustomerId customerId) {
return store.values().stream()
.filter(o -> o.getCustomerId().equals(customerId))
.collect(Collectors.toList());
}
@Override
public void save(Order order) {
store.put(order.getId().getValue(), order);
}
@Override
public void delete(OrderId id) {
store.remove(id.getValue());
}
@Override
public List<Order> findByStatus(OrderStatus status) {
return store.values().stream()
.filter(o -> o.getStatus() == status)
.collect(Collectors.toList());
}
}8.2 Python 示例:仓储实现
from abc import ABC, abstractmethod
from typing import Optional, List
import sqlite3
import json
class OrderRepository(ABC):
"""订单仓储接口"""
@abstractmethod
def find_by_id(self, order_id: str) -> Optional['Order']:
pass
@abstractmethod
def find_by_customer_id(self, customer_id: str) -> List['Order']:
pass
@abstractmethod
def save(self, order: 'Order'):
pass
@abstractmethod
def delete(self, order_id: str):
pass
class InMemoryOrderRepository(OrderRepository):
"""内存仓储实现(用于测试)"""
def __init__(self):
self._store: dict[str, 'Order'] = {}
def find_by_id(self, order_id: str) -> Optional['Order']:
return self._store.get(order_id)
def find_by_customer_id(self, customer_id: str) -> List['Order']:
return [
order for order in self._store.values()
if order.customer_id.value == customer_id
]
def save(self, order: 'Order'):
self._store[order.id.value] = order
def delete(self, order_id: str):
self._store.pop(order_id, None)
class SqliteOrderRepository(OrderRepository):
"""基于 SQLite 的仓储实现"""
def __init__(self, db_path: str):
self._db_path = db_path
self._init_db()
def _init_db(self):
"""初始化数据库表"""
conn = sqlite3.connect(self._db_path)
cursor = conn.cursor()
cursor.execute('''
CREATE TABLE IF NOT EXISTS orders (
id TEXT PRIMARY KEY,
customer_id TEXT NOT NULL,
status TEXT NOT NULL,
total_amount REAL NOT NULL,
currency TEXT NOT NULL,
items_json TEXT NOT NULL
)
''')
conn.commit()
conn.close()
def find_by_id(self, order_id: str) -> Optional['Order']:
conn = sqlite3.connect(self._db_path)
cursor = conn.cursor()
cursor.execute('SELECT * FROM orders WHERE id = ?', (order_id,))
row = cursor.fetchone()
conn.close()
if row is None:
return None
return self._row_to_order(row)
def find_by_customer_id(self, customer_id: str) -> List['Order']:
conn = sqlite3.connect(self._db_path)
cursor = conn.cursor()
cursor.execute('SELECT * FROM orders WHERE customer_id = ?', (customer_id,))
rows = cursor.fetchall()
conn.close()
return [self._row_to_order(row) for row in rows]
def save(self, order: 'Order'):
conn = sqlite3.connect(self._db_path)
cursor = conn.cursor()
# 序列化订单项
items_data = [
{
'product_id': item.product_id.value,
'product_name': item.product_name,
'unit_price': item.unit_price.amount,
'quantity': item.quantity
}
for item in order.items
]
items_json = json.dumps(items_data)
cursor.execute('''
INSERT OR REPLACE INTO orders
(id, customer_id, status, total_amount, currency, items_json)
VALUES (?, ?, ?, ?, ?, ?)
''', (
order.id.value,
order.customer_id.value,
order.status.value,
order.total_amount,
order.currency,
items_json
))
conn.commit()
conn.close()
def delete(self, order_id: str):
conn = sqlite3.connect(self._db_path)
cursor = conn.cursor()
cursor.execute('DELETE FROM orders WHERE id = ?', (order_id,))
conn.commit()
conn.close()
def _row_to_order(self, row) -> 'Order':
"""将数据库行转换为领域对象"""
from domain.order import Order, OrderId, CustomerId, OrderItem, ProductId, Money
order_id, customer_id, status, total_amount, currency, items_json = row
order = Order(OrderId(order_id), CustomerId(customer_id))
# 恢复订单项
items_data = json.loads(items_json)
for item_data in items_data:
item = OrderItem(
ProductId(item_data['product_id']),
item_data['product_name'],
Money(item_data['unit_price'], currency),
item_data['quantity']
)
order.add_item_directly(item)
# 恢复状态
from domain.order import OrderStatus
order.set_status_for_rebuild(OrderStatus(status))
return order8.3 查询服务示例
/**
* 订单查询服务
* 专门负责查询,返回 DTO 而非领域对象
*/
@Service
public class OrderQueryService {
private final JdbcTemplate jdbcTemplate;
public OrderQueryService(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
/**
* 查询订单列表(支持过滤、排序、分页)
*/
public Page<OrderSummaryDTO> listOrders(OrderQuery query) {
// 构建 SQL
String sql = buildQuerySql(query);
String countSql = buildCountSql(query);
// 查询总数
long total = jdbcTemplate.queryForObject(countSql, Long.class);
// 查询数据
List<OrderSummaryDTO> orders = jdbcTemplate.query(sql, new OrderSummaryRowMapper());
return new PageImpl<>(orders, query.getPageable(), total);
}
/**
* 查询订单详情
*/
public OrderDetailDTO getOrderDetail(OrderId orderId) {
String sql = "SELECT o.*, c.name as customer_name FROM orders o " +
"JOIN customers c ON o.customer_id = c.id " +
"WHERE o.id = ?";
return jdbcTemplate.queryForObject(sql, new OrderDetailRowMapper(), orderId.getValue());
}
/**
* 查询订单统计
*/
public OrderStatisticsDTO getOrderStatistics(LocalDate startDate, LocalDate endDate) {
String sql = "SELECT COUNT(*) as order_count, SUM(total_amount) as total_revenue " +
"FROM orders WHERE created_at BETWEEN ? AND ?";
return jdbcTemplate.queryForObject(sql, new OrderStatisticsRowMapper(),
startDate, endDate);
}
}
/**
* 订单摘要 DTO
*/
public class OrderSummaryDTO {
private String orderId;
private String customerName;
private String status;
private BigDecimal totalAmount;
private LocalDateTime createdAt;
// Getters and Setters
public String getOrderId() { return orderId; }
public void setOrderId(String orderId) { this.orderId = orderId; }
public String getCustomerName() { return customerName; }
public void setCustomerName(String customerName) { this.customerName = customerName; }
public String getStatus() { return status; }
public void setStatus(String status) { this.status = status; }
public BigDecimal getTotalAmount() { return totalAmount; }
public void setTotalAmount(BigDecimal totalAmount) { this.totalAmount = totalAmount; }
public LocalDateTime getCreatedAt() { return createdAt; }
public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
}
/**
* 订单查询参数
*/
public class OrderQuery {
private CustomerId customerId;
private OrderStatus status;
private LocalDate startDate;
private LocalDate endDate;
private Pageable pageable;
// Getters and Setters
public CustomerId getCustomerId() { return customerId; }
public void setCustomerId(CustomerId customerId) { this.customerId = customerId; }
public OrderStatus getStatus() { return status; }
public void setStatus(OrderStatus status) { this.status = status; }
public LocalDate getStartDate() { return startDate; }
public void setStartDate(LocalDate startDate) { this.startDate = startDate; }
public LocalDate getEndDate() { return endDate; }
public void setEndDate(LocalDate endDate) { this.endDate = endDate; }
public Pageable getPageable() { return pageable; }
public void setPageable(Pageable pageable) { this.pageable = pageable; }
}五、深入思考
8.1 历史背景
仓储模式源于 Martin Fowler 的《Patterns of Enterprise Application Architecture》。Fowler 将仓储定义为"模拟集合的领域对象访问机制"。
Eric Evans 在《领域驱动设计》中进一步阐述了仓储的概念,强调仓储与聚合的关系。他指出,仓储的操作单位应该是聚合,而不是单个实体或表。
随着 ORM 框架的普及,仓储模式得到了广泛应用。Hibernate、Entity Framework 等框架提供了类似仓储的 API,简化了数据访问。
近年来,随着 CQRS 和事件溯源的兴起,仓储的角色发生了变化。在 CQRS 中,仓储主要负责写操作,读操作由专门的查询服务处理。
8.2 前沿方向
仓储的实践正在向以下几个方向发展:
首先是泛型仓储的普及。通过泛型基类减少重复代码,提高开发效率。但需要注意,泛型仓储不应该替代特定业务的仓储接口。
其次是异步仓储。随着响应式编程的流行,仓储接口开始支持异步操作,返回 CompletableFuture 或 Mono/Flux。
第三是多存储后端的支持。现代应用可能需要同时支持关系数据库、文档数据库、搜索引擎等多种存储。仓储抽象使得切换存储后端变得更加容易。
第四是与云原生技术的结合。仓储可以实现对云数据库、对象存储、NoSQL 等云服务的适配。
8.3 延伸阅读
Eric Evans, 《领域驱动设计》第6章 - 原始论述
Vaughn Vernon, 《实现领域驱动设计》第11章 - 实用指南
Martin Fowler, 《Repository》- Patterns of Enterprise Application Architecture
Greg Young, 《CQRS》- 论文
六、实验笔记
实验一:为现有系统设计仓储
实验目的:通过分析现有系统,练习设计仓储接口和实现。
实验步骤:
选择一个现有项目,识别其中的聚合根。
为每个聚合根设计仓储接口,定义在领域层。
实现至少两种仓储:内存实现(用于测试)和数据库实现。
编写单元测试,验证仓储的正确性。
比较不同实现的优缺点。
预期收获:掌握仓储设计的完整流程,理解接口与实现分离的好处。
实验二:实现 CQRS 风格的读写分离
实验目的:练习将读写分离,仓储负责写,查询服务负责读。
实验步骤:
设计仓储接口,只包含保存、查找、删除等基本操作。
设计查询服务,支持复杂查询、过滤、排序、分页。
查询服务返回 DTO,不是领域对象。
实现一个完整的用例:通过仓储写入数据,通过查询服务读取数据。
分析读写分离带来的好处和挑战。
预期收获:理解 CQRS 的思想,掌握读写分离的实现技巧。
七、本章小结
本章详细介绍了 DDD 中的仓储模式。我们首先定义了仓储的本质:聚合持久化的抽象,提供类似集合的接口。然后,我们探讨了仓储与 DAO 的区别:仓储面向领域,DAO 面向数据。
仓储的职责包括保存聚合、检索聚合、删除聚合和提供基本查询。仓储不应该包含业务逻辑,那是聚合和领域服务的职责。
仓储接口应该定义在领域层,使用领域类型,保持简洁。仓储的实现位于基础设施层,可以使用各种持久化技术。
仓储与查询的分离是本章的重点内容。通过 CQRS 模式,写操作使用仓储保证一致性,读操作使用查询服务提供高效的数据访问。
通过代码实战,我们展示了如何实现仓储接口、JPA 实现、内存实现和查询服务。这些示例体现了仓储的最佳实践:接口与实现分离、面向聚合、读写分离。
最后,我们回顾了仓储的历史背景,展望了前沿方向,并提供了两个实验任务帮助读者实践所学内容。
仓储是连接领域层和基础设施层的桥梁。正确设计仓储可以保护领域模型的纯净性,提高代码的可测试性和可维护性。希望读者能够在实际项目中应用本章的知识,设计出优雅的仓储。
八、习题
概念理解题:用自己的话解释什么是仓储,它的核心职责是什么?
辨析题:仓储与 DAO 有什么区别?为什么说仓储是更高层次的抽象?
设计题:为一个博客系统设计仓储接口。包括文章仓储、评论仓储等。定义在领域层,使用领域类型。
代码分析题:以下仓储设计有什么问题?如何改进?
public interface OrderRepository {
List<Map<String, Object>> findOrdersWithJoin(); // 返回技术类型
void executeNativeSql(String sql); // 暴露技术细节
Order findById(Long id); // 使用技术类型
}思考题:为什么仓储的操作单位应该是聚合,而不是单个实体?
实践题:在你当前参与的项目中,找出数据访问的代码,分析是否可以重构为仓储模式。
论述题:仓储是否应该支持复杂查询?在什么情况下应该将查询分离到专门的服务中?
设计题:设计一个支持乐观锁的仓储实现。包括版本号的存储和并发冲突的处理。
开放题:在微服务架构中,仓储的设计有什么特殊考虑?是否需要为每个微服务设计独立的仓储?
综合题:为一个电商系统设计完整的仓储层。包括订单仓储、产品仓储、库存仓储、客户仓储等。画出类图,说明每个仓储的职责和方法。