领域驱动设计(DDD)——仓储(Repository)详解和示例

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

DDD架构
DDD架构

       

 以下是对仓储的详细解释:


仓储的定义

仓储(Repository)的核心思想是:

提供一个中间层,让应用程序可以像操作内存中的对象集合一样操作领域对象,而不关心对象的持久化实现细节。

仓储是一个领域对象集合的抽象,应用程序可以通过它来存储、查询、更新、删除领域对象。


仓储的设计原则

  1. 隐藏持久化细节:
    仓储屏蔽了底层存储的复杂性(如 SQL 查询、事务管理等),领域层只需要通过仓储接口与之交互,而无需关心存储的实现细节。

  2. 提供领域模型的聚合操作:
    仓储通常与聚合根(Aggregate Root)紧密相关。每个聚合(Aggregate)通常会有一个对应的仓储,仓储只负责管理聚合根及其聚合内的对象。

  3. 保持统一接口:
    仓储通过接口定义了对领域对象的基本操作,比如添加、删除、查找和更新等操作。


仓储的职责

  1. 存储领域对象:
    负责将领域对象保存到持久化存储(如数据库)。

  2. 获取领域对象:
    提供方法来根据条件查询领域对象(如通过 ID 查找订单)。

  3. 删除领域对象:
    负责从存储中移除领域对象。

  4. 隐藏存储细节:
    将底层存储逻辑封装起来(如 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
订单未找到!

仓储的核心原理

  1. 隐藏存储细节:

    • 仓储封装了底层存储的逻辑(如保存、查找、删除等),使得领域模型不需要直接依赖数据库操作。
  2. 像操作集合一样操作领域对象:

    • 仓储让我们能够像操作内存中的集合一样操作领域对象,而底层实现可能使用数据库、文件存储等方式。
  3. 与聚合根紧密关联:

    • 仓储只负责管理聚合根,聚合根内部的其他对象由聚合根本身管理。例如,Order 是聚合根,仓储只操作 Order,而不会直接操作订单中的明细条目。
  4. 实现领域逻辑与基础设施的解耦:

    • 通过接口定义仓储,领域层只依赖仓储接口,具体的实现由基础设施层提供。这种设计符合 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)与数据库操作解耦,生产上通过以下方式实现代码:

  1. 定义领域模型:表示业务数据和逻辑。
  2. 定义仓储接口:提供统一的领域对象操作方法。
  3. 使用 MyBatis Mapper:通过 XML 或注解定义 SQL,完成数据库交互。
  4. 仓储实现:通过 MyBatis 的 Mapper 动态代理实现仓储接口。

        这种实现方式符合 DDD 的设计原则,同时充分利用 MyBatis 强大的 SQL 映射能力,是生产环境中一种高效且可靠的实现方式。


总结

        在 DDD 设计原则中,仓储的作用是为领域模型提供对持久化存储的访问,同时隐藏存储细节,从而实现领域逻辑与基础设施的解耦。通过仓储模式,我们能够更方便地操作领域对象,同时保持代码的清晰性和可扩展性。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值