8领域驱动设计

仓储

聚合的持久化

仓储集合持久化

🎯 AI 时代的学习建议:不要只读理论。理解这一章的设计动机(为什么需要这个概念?)和边界条件(什么情况下会失效?)。当 AI 生成的代码涉及这些概念时,你能判断它是否用对了场景。AI 能帮你总结内容,但无法替代你亲手阅读、思考、建立直觉的过程。

第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 仓储接口的设计

仓储接口应该定义在领域层,使用业务语言,不暴露技术细节。一个好的仓储接口应该具备以下特点:

简洁

仓储接口应该尽可能简洁,只提供必要的方法。典型的方法包括:

java
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 仓储与事务

仓储本身不应该管理事务,事务管理是应用层的职责。应用服务开启事务,调用仓储保存聚合,然后提交或回滚事务。

典型的流程如下:

java
@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 仓储的版本控制

在一些场景中,需要处理聚合的并发修改。常见的策略包括:

乐观锁

在聚合中添加版本号字段,每次更新时检查版本号。如果版本号不匹配,说明有其他修改,抛出异常。

java
public class Order {
    private int version;
    
    public void incrementVersion() {
        this.version++;
    }
}

仓储在保存时检查版本号:

java
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 示例:仓储接口与实现

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 示例:仓储实现

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 order

8.3 查询服务示例

java
/**
 * 订单查询服务
 * 专门负责查询,返回 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 有什么区别?为什么说仓储是更高层次的抽象?

设计题:为一个博客系统设计仓储接口。包括文章仓储、评论仓储等。定义在领域层,使用领域类型。

代码分析题:以下仓储设计有什么问题?如何改进?

java
public interface OrderRepository {
    List<Map<String, Object>> findOrdersWithJoin();  // 返回技术类型
    void executeNativeSql(String sql);  // 暴露技术细节
    Order findById(Long id);  // 使用技术类型
}

思考题:为什么仓储的操作单位应该是聚合,而不是单个实体?

实践题:在你当前参与的项目中,找出数据访问的代码,分析是否可以重构为仓储模式。

论述题:仓储是否应该支持复杂查询?在什么情况下应该将查询分离到专门的服务中?

设计题:设计一个支持乐观锁的仓储实现。包括版本号的存储和并发冲突的处理。

开放题:在微服务架构中,仓储的设计有什么特殊考虑?是否需要为每个微服务设计独立的仓储?

综合题:为一个电商系统设计完整的仓储层。包括订单仓储、产品仓储、库存仓储、客户仓储等。画出类图,说明每个仓储的职责和方法。

8/12仓储
0%