DDD从0到企业级:迭代式学习 (共17章)之 三

「鸿蒙心迹」“2025・领航者闯关记“主题征文活动 10w+人浏览 476人参与

“领域模型画完了,限界上下文也定了,但一写代码就打回原形——领域层里全是MyBatis注解,业务逻辑和SQL语句搅在一起”——这是DDD落地中最常见的“断层”问题。第二阶段我们完成了业务蓝图的绘制,第三阶段的核心就是搭建“翻译桥梁”,把领域模型转化为符合企业级规范的代码架构,解决“模型与代码两张皮”的痛点。本阶段共4篇文章,从架构设计到模式落地,全程配套可复用的Java代码模板与工程规范。

第一篇:六边形架构:让领域逻辑不依赖任何框架

“用Spring Boot开发就必须把@Service放在领域层吗?”“换个数据库就要改遍业务代码,这到底是哪里出了问题?”——很多团队在DDD落地时,会被框架绑架,导致领域逻辑失去独立性。而六边形架构(又称端口-适配器架构)的核心价值,就是让领域层“跳出框架束缚”,成为系统真正的核心。

一、为什么传统三层架构会导致“代码腐化”?

传统MVC三层架构(控制层→服务层→数据访问层)的最大问题,是领域逻辑被技术细节包裹:

  • 服务层既包含业务规则(如订单价格计算),又依赖数据访问层的MyBatis接口,业务与技术高度耦合。

  • 当需要对接第三方支付(如从微信支付换成支付宝)或更换数据库(MySQL→MongoDB)时,会穿透多层修改代码,风险极高。

  • 领域逻辑分散在各个Service方法中,无法形成独立的业务资产,新功能开发只能“堆代码”。

而六边形架构通过“端口”和“适配器”,在领域层与外部依赖之间建立隔离墙,让领域逻辑只关注“业务是什么”,不关心“技术怎么实现”。

二、六边形架构四层拆解:以电商订单系统为例

六边形架构以领域层为核心,向外辐射端口层、适配器层、应用层,每层职责清晰,依赖关系严格遵循“内层不依赖外层”原则。我们以Spring Boot工程为例,拆解各层的职责与代码结构:

1. 核心层:领域层(Domain)——业务规则的“大本营”

领域层是系统的核心,包含实体、值对象、聚合根、领域服务,只依赖JDK,不引入任何框架依赖(如Spring、MyBatis)。它定义“业务能做什么”,不关心“如何实现”。

订单领域层代码结构:

com.ddd.order.domain
├── aggregate  // 聚合根
│   └── Order.java  // 订单聚合根(含cancel()等业务方法)
├── entity     // 实体(非聚合根)
│   └── OrderItem.java  // 订单项实体
├── valueobject  // 值对象
│   └── Address.java    // 收货地址值对象
├── service    // 领域服务(跨聚合业务)
│   └── OrderPriceCalculateService.java  // 订单价格计算(依赖商品领域)
└── repository // 仓储端口(领域定义接口)
    └── OrderRepository.java  // 订单仓储接口(不包含实现)

关键亮点:领域层的OrderRepository是接口,只定义“需要获取订单数据”的能力(如findById、save),不包含任何SQL或框架注解,这就是“端口”的核心作用——定义领域对外的交互规则。

2. 端口层:领域对外的“交互规则”

端口层是领域层对外暴露的“接口集合”,分为两类:

  • 输入端口:供外部调用领域功能的接口,如“创建订单”“取消订单”,通常由应用服务实现。

  • 输出端口:领域依赖外部资源的接口,如仓储接口(OrderRepository)、第三方服务接口(PaymentService),由适配器层实现。

端口层的价值是“解耦依赖方向”——不是领域层依赖外部,而是外部通过端口对接领域,比如订单领域需要查询商品价格,就定义一个ProductPort接口,由商品领域的适配器实现。

3. 适配器层:连接领域与外部的“翻译官”

适配器层负责实现端口层的接口,将外部依赖(如数据库、第三方服务、API接口)“翻译”为领域能理解的语言,同时也将领域的输出“翻译”为外部能接受的格式。常见的适配器类型:

适配器类型

职责说明

代码示例(Spring Boot)

仓储适配器

实现OrderRepository,对接数据库

@Repository注解的OrderRepositoryMyBatis

外部服务适配器

实现PaymentPort,对接第三方支付

@Service注解的PaymentAdapter

API适配器

将领域模型转化为DTO,供Controller调用

OrderDTOConverter(转换工具类)

4. 应用层:流程编排的“指挥官”

应用层不包含业务规则,只负责将领域层的功能按业务流程串联起来,比如“创建订单”流程需要调用“价格计算”“库存校验”“支付发起”等领域功能,应用层就负责协调这些操作的顺序和事务。

应用层代码示例:


@Service
public class OrderApplicationService {
    // 依赖领域层的仓储端口(接口)
    private final OrderRepository orderRepository;
    // 依赖领域服务
    private final OrderPriceCalculateService priceCalculateService;
    // 依赖外部服务端口
    private final PaymentPort paymentPort;

    // 构造器注入(避免字段注入的耦合问题)
    public OrderApplicationService(OrderRepository orderRepository, 
                                   OrderPriceCalculateService priceCalculateService,
                                   PaymentPort paymentPort) {
        this.orderRepository = orderRepository;
        this.priceCalculateService = priceCalculateService;
        this.paymentPort = paymentPort;
    }

    // 订单创建流程(应用服务核心:编排流程)
    @Transactional
    public OrderDTO createOrder(OrderCreateCommand command) {
        // 1. 转换DTO为领域模型(通过适配器)
        Address address = new Address(command.getProvince(), command.getCity(), command.getDetail());
        List<OrderItem> orderItems = command.getItems().stream()
                .map(item -> new OrderItem(new ProductId(item.getProductId()), item.getQuantity()))
                .collect(Collectors.toList());
        
        // 2. 调用领域服务计算价格(业务规则在领域层)
        BigDecimal totalPrice = priceCalculateService.calculate(orderItems);
        
        // 3. 构建聚合根(领域逻辑)
        Order order = new Order(new OrderId(UUID.randomUUID().toString()), 
                               orderItems, address, totalPrice);
        
        // 4. 调用仓储端口保存(实现由适配器提供)
        Order savedOrder = orderRepository.save(order);
        
        // 5. 调用外部服务发起支付(实现由适配器提供)
        paymentPort.initPayment(savedOrder.getId().getValue(), totalPrice);
        
        // 6. 转换领域模型为DTO返回(适配器)
        return OrderDTOConverter.toDTO(savedOrder);
    }
}

三、企业级工程规范:包结构与依赖原则

六边形架构落地的关键是严格遵守包结构和依赖规则,避免“越界调用”。企业级Spring Boot工程的标准包结构:


com.ddd.order  // 应用根包
├── application  // 应用层
│   ├── service  // 应用服务
│   ├── command  // 命令对象(接收前端参数)
│   └── dto      // 数据传输对象
├── domain       // 领域层(核心,无框架依赖)
│   ├── aggregate
│   ├── entity
│   ├── valueobject
│   ├── service
│   └── port     // 端口(输入+输出)
├── infrastructure  // 基础设施层(适配器)
│   ├── repository  // 仓储适配器(MyBatis实现)
│   ├── client      // 外部服务适配器(Feign调用)
│   └── converter   // DTO-领域模型转换器
└── interface      // 接口层(Controller)
    └── rest       // REST接口

核心依赖原则:内层包不依赖外层包!即domain层不能依赖application、infrastructure、interface层;application层只能依赖domain层;infrastructure层依赖domain层的端口;interface层依赖application层。

通过六边形架构,当需要更换数据库时,只需新增一个实现OrderRepository的MongoDB适配器,无需修改领域层一行代码;当需要对接新的支付渠道时,只需实现PaymentPort的新适配器——这就是企业级系统“高内聚、低耦合”的核心实现方式。

第二篇:仓储模式:领域与数据库的“隔离墙”怎么建?

“领域模型里直接写JPA注解很方便,为什么还要搞个仓储接口?”——这是很多开发者对仓储模式的疑问。但在企业级系统中,“方便”往往意味着“后期灾难”:当业务从单体迁移到微服务,或从关系型数据库迁移到分布式数据库时,直接耦合数据库的领域模型会让修改成本呈指数级增长。仓储模式的核心,就是在领域与数据库之间建立“隔离墙”。

一、仓储模式的本质:领域定义“需求”,技术实现“供给”

DDD中的仓储(Repository)不是传统的DAO层,它有两个核心定位:

  • 领域的“数据访问代理人”:领域层通过仓储接口,提出“需要获取/保存订单数据”的需求,不关心数据存在MySQL还是MongoDB,也不关心用MyBatis还是JPA。

  • 聚合的“完整性守护者”:仓储操作的最小单位是“聚合根”,而不是单个实体。比如保存订单时,会自动保存聚合内的订单项和地址,确保聚合的完整性。

举个通俗的例子:你(领域层)想吃火锅(需要数据),不需要自己去菜市场买菜(操作数据库),只需告诉外卖员(仓储接口)你的需求,外卖员(仓储适配器)会去不同的菜市场(不同数据库)帮你完成,你只需要等待结果即可。

二、仓储模式三步落地:以订单仓储为例

仓储模式的落地分为“定义端口”“实现适配器”“使用仓储”三步,全程遵循“领域驱动技术”的原则。

1. 第一步:在领域层定义仓储端口(接口)

仓储端口属于领域层,只定义“业务需要的数据操作能力”,不包含任何技术细节。订单仓储接口需满足:按ID查询订单、保存订单、按用户ID查询待支付订单这三个核心业务需求。


// 领域层端口:com.ddd.order.domain.port.repository.OrderRepository
public interface OrderRepository {
    /**
     * 按订单ID查询订单聚合
     * 注:返回的是聚合根,包含完整的订单项和地址
     */
    Optional<Order> findById(OrderId orderId);

    /**
     * 保存订单聚合
     * 注:自动保存聚合内的所有实体和值对象
     */
    Order save(Order order);

    /**
     * 按用户ID查询待支付订单
     * 注:业务场景驱动的查询方法,而非通用的findAll
     */
    List<Order> findPendingPaymentByUserId(UserId userId);
}

// 领域层值对象:订单ID(封装ID逻辑)
public class OrderId {
    private String value;

    public OrderId(String value) {
        // 领域规则:订单ID必须是32位UUID
        if (value == null || value.length() != 32) {
            throw new BusinessException("订单ID格式错误");
        }
        this.value = value;
    }

    // 只暴露getter,无setter,确保不可变
    public String getValue() {
        return value;
    }
}

关键设计:仓储接口的参数和返回值都是领域模型(Order、OrderId),而非DTO或数据库实体,确保领域层的独立性。同时,方法名是“业务化”的(如findPendingPaymentByUserId),而非技术化的(如selectByUserIdAndStatus),体现领域驱动的思想。

2. 第二步:在基础设施层实现仓储适配器

适配器层负责将领域的仓储接口,通过具体的技术框架(如MyBatis)实现。这里需要解决两个核心问题:领域模型与数据库实体的映射、聚合完整性的保证。

(1)定义数据库实体(PO)

数据库实体(PO)属于基础设施层,只与数据库表结构对应,不包含任何业务逻辑。订单相关的数据库表有t_order(订单主表)、t_order_item(订单项表),对应的PO:


// 基础设施层PO:com.ddd.order.infrastructure.repository.po.OrderPo
@Data
@TableName("t_order")
public class OrderPo {
    @TableId(type = IdType.INPUT)
    private String orderId; // 对应OrderId的value
    private String userId;
    private BigDecimal totalPrice;
    private String status; // 存储状态码,如"PAID"
    private String province; // 地址信息(冗余存储,便于查询)
    private String city;
    private String detail;
    private LocalDateTime createTime;

    // 关联订单项PO(MyBatis-Plus的关联查询)
    @TableField(exist = false)
    private List<OrderItemPo> orderItemPos;
}

@Data
@TableName("t_order_item")
public class OrderPo {
    @TableId(type = IdType.AUTO)
    private Long id;
    private String orderId;
    private String productId;
    private Integer quantity;
    private BigDecimal price;
}
(2)实现仓储接口(MyBatis适配器)

适配器通过MyBatis-Plus操作数据库,将PO转化为领域模型,确保领域层完全不感知数据库的存在。


// 基础设施层适配器:com.ddd.order.infrastructure.repository.OrderRepositoryMyBatis
@Repository
public class OrderRepositoryMyBatis implements OrderRepository {

    private final OrderMapper orderMapper;
    private final OrderItemMapper orderItemMapper;

    // 构造器注入Mapper
    public OrderRepositoryMyBatis(OrderMapper orderMapper, OrderItemMapper orderItemMapper) {
        this.orderMapper = orderMapper;
        this.orderItemMapper = orderItemMapper;
    }

    @Override
    public Optional<Order> findById(OrderId orderId) {
        // 1. 查询订单主表PO
        OrderPo orderPo = orderMapper.selectById(orderId.getValue());
        if (orderPo == null) {
            return Optional.empty();
        }
        // 2. 查询关联的订单项PO
        List<OrderItemPo> itemPos = orderItemMapper.selectByOrderId(orderId.getValue());
        // 3. PO转化为领域模型(通过转换器)
        return Optional.of(OrderConverter.toDomain(orderPo, itemPos));
    }

    @Override
    public Order save(Order order) {
        // 1. 领域模型转化为PO
        OrderPo orderPo = OrderConverter.toPo(order);
        List<OrderItemPo> itemPos = OrderConverter.toItemPos(order.getOrderItems(), order.getId().getValue());
        
        // 2. 保存订单主表(新增/更新)
        if (orderMapper.existsById(order.getId().getValue())) {
            orderMapper.updateById(orderPo);
            // 先删后增,保证订单项与订单一致(聚合完整性)
            orderItemMapper.deleteByOrderId(order.getId().getValue());
        } else {
            orderMapper.insert(orderPo);
        }
        // 3. 保存订单项(聚合内强一致性)
        orderItemMapper.batchInsert(itemPos);
        
        // 4. 返回保存后的领域模型
        return this.findById(order.getId()).orElseThrow();
    }

    @Override
    public List<Order> findPendingPaymentByUserId(UserId userId) {
        List<OrderPo> orderPos = orderMapper.selectByUserIdAndStatus(
                userId.getValue(), OrderStatus.PENDING_PAYMENT.getCode());
        return orderPos.stream()
                .map(po -> {
                    List<OrderItemPo> itemPos = orderItemMapper.selectByOrderId(po.getOrderId());
                    return OrderConverter.toDomain(po, itemPos);
                })
                .collect(Collectors.toList());
    }
}
3. 第三步:依赖注入,领域层使用仓储

领域层和应用层通过依赖注入使用仓储接口,完全不依赖具体的实现类。比如应用服务调用仓储:


@Service
public class OrderApplicationService {
    // 依赖领域层的接口,而非基础设施层的实现
    private final OrderRepository orderRepository;

    // 构造器注入(Spring会自动注入MyBatis实现)
    public OrderApplicationService(OrderRepository orderRepository) {
        this.orderRepository = orderRepository;
    }

    // 订单取消后更新订单
    public void cancelOrder(String orderId) {
        Order order = orderRepository.findById(new OrderId(orderId))
                .orElseThrow(() -> new BusinessException("订单不存在"));
        // 调用领域模型的业务方法
        order.cancel();
        // 调用仓储保存
        orderRepository.save(order);
    }
}

三、企业级仓储优化:解决聚合查询的N+1问题

仓储模式中最常见的性能问题是“N+1查询”(查询N个订单,再分别查询每个订单的订单项)。企业级解决方案有两种:

  1. 关联查询+结果映射:通过MyBatis的resultMap配置,一次SQL查询出订单和订单项,避免多次查询。示例:

    <resultMap id="OrderWithItemsMap" type="com.ddd.order.infrastructure.repository.po.OrderPo">
        <id column="order_id" property="orderId"/>
        <result column="user_id" property="userId"/&gt;
        <!-- 其他字段映射 -->
        <!-- 关联订单项集合 -->
        <collection property="orderItemPos" ofType="com.ddd.order.infrastructure.repository.po.OrderItemPo">
            <id column="item_id" property="id"/>
            <result column="product_id" property="productId"/&gt;
            <!-- 其他订单项字段映射 -->
        </collection>
    </resultMap>
    
    <select id="selectByIdWithItems" resultMap="OrderWithItemsMap">
        SELECT o.*, i.id as item_id, i.product_id, i.quantity
        FROM t_order o
        LEFT JOIN t_order_item i ON o.order_id = i.order_id
        WHERE o.order_id = #{orderId}
    </select>

  2. 读写分离:查询操作走只读数据源,使用关联查询;写入操作走主数据源,确保数据一致性。通过Spring的@Transactional(readOnly = true)实现。

仓储模式的核心价值,是让领域模型“摆脱数据库的束缚”,成为真正的业务资产。在企业级系统中,这种“隔离”带来的不仅是技术灵活性,更是业务快速迭代的能力。

第三篇:应用服务设计:不写业务逻辑的“流程指挥官”

“应用服务和领域服务到底有什么区别?”“订单创建的逻辑该放在应用服务还是领域服务?”——这是DDD落地中最容易混淆的问题。很多团队把所有业务逻辑都堆在应用服务里,导致应用服务变成“大泥球”;也有团队把流程编排逻辑放进领域服务,导致领域服务依赖外部资源。其实两者的边界很清晰:应用服务是“流程指挥官”,领域服务是“业务专家”。

一、应用服务与领域服务的核心区别

要明确两者的边界,首先要理解它们的定位差异。我们用电商“创建订单”流程举例,对比两者的职责:

对比维度

应用服务(Application Service)

领域服务(Domain Service)

核心定位

流程编排者,负责“怎么做”

业务规则实现者,负责“是什么”

依赖对象

依赖领域服务、仓储接口、外部服务端口

只依赖领域模型(实体、值对象),无外部依赖

业务范围

跨领域/跨聚合的流程(如创建订单需调用支付服务)

单领域内的业务规则(如订单价格计算)

示例逻辑

1. 接收前端参数→2. 转换为领域模型→3. 调用价格计算→4. 保存订单→5. 发起支付

根据商品单价、数量、优惠券计算订单总价

记忆口诀:应用服务管“流程”,领域服务管“规则”;应用服务对外“协调资源”,领域服务对内“实现业务”。

二、应用服务设计三原则:避免“大泥球”

企业级应用服务设计必须遵守三个核心原则,否则容易陷入“逻辑混乱、难以维护”的困境。

1. 原则一:只做流程编排,不写业务规则

应用服务的代码应该是“线性的流程步骤”,而不是包含复杂条件判断的业务逻辑。反例与正例对比:


// 反例:应用服务包含业务规则(价格计算)
@Service
public class BadOrderApplicationService {
    public OrderDTO createOrder(OrderCreateCommand command) {
        // 错误:价格计算的业务规则放在了应用服务
        BigDecimal totalPrice = BigDecimal.ZERO;
        for (OrderItemCommand item : command.getItems()) {
            // 商品单价从外部接口获取(应用服务依赖外部资源)
            BigDecimal productPrice = productClient.getPrice(item.getProductId());
            totalPrice = totalPrice.add(productPrice.multiply(new BigDecimal(item.getQuantity())));
        }
        // 错误:订单状态判断的业务规则也在应用服务
        OrderStatus status = command.getPayType() == PayType.NOW ? PENDING_PAYMENT : UNPAID;
        // ... 后续逻辑
    }
}

// 正例:应用服务只做流程编排
@Service
public class GoodOrderApplicationService {
    private final OrderPriceCalculateService priceService; // 领域服务
    private final OrderRepository orderRepository; // 仓储接口
    private final PaymentPort paymentPort; // 外部服务端口

    public OrderDTO createOrder(OrderCreateCommand command) {
        // 1. 转换参数为领域模型(流程步骤)
        List<OrderItem> items = convertToOrderItems(command);
        // 2. 调用领域服务计算价格(业务规则交给领域)
        BigDecimal totalPrice = priceService.calculate(items, command.getCouponId());
        // 3. 调用领域模型构建订单(状态规则在领域)
        Order order = Order.create(items, convertToAddress(command), totalPrice);
        // 4. 调用仓储保存(数据操作交给仓储)
        Order savedOrder = orderRepository.save(order);
        // 5. 调用外部服务发起支付(外部交互交给端口)
        paymentPort.initPayment(savedOrder.getId().getValue(), totalPrice);
        // 6. 转换为DTO返回(流程收尾)
        return convertToDTO(savedOrder);
    }
}
2. 原则二:一个应用服务对应一个业务场景

避免设计“万能应用服务”,比如一个OrderApplicationService包含创建订单、取消订单、查询订单、修改订单等所有逻辑。正确的做法是按业务场景拆分:


// 按场景拆分应用服务
com.ddd.order.application.service
├── OrderCreationService.java  // 订单创建场景
├── OrderCancellationService.java  // 订单取消场景
├── OrderQueryService.java  // 订单查询场景
└── OrderModificationService.java  // 订单修改场景

这样拆分的好处是:职责单一,便于维护;不同场景可以独立扩展(如查询场景需要优化性能,不影响创建场景);符合“微服务按场景拆分”的演进方向。

3. 原则三:通过“命令对象”接收参数,避免DTO泛滥

应用服务接收前端参数时,应使用“命令对象”(Command)而非通用DTO。命令对象与DTO的区别:命令对象是“动词性”的,对应一个具体操作(如OrderCreateCommand),只包含该操作需要的参数;DTO是“名词性”的,用于数据传输。


// 命令对象:只包含创建订单需要的参数
public class OrderCreateCommand {
    @NotNull(message = "用户ID不能为空")
    private String userId;
    @NotEmpty(message = "订单项不能为空")
    private List<OrderItemCommand> items;
    private String couponId; // 可选参数(优惠券)
    @NotNull(message = "收货地址不能为空")
    private String province;
    private String city;
    private String detail;

    // getter + setter
}

// 订单项命令对象(嵌套)
public class OrderItemCommand {
    @NotNull(message = "商品ID不能为空")
    private String productId;
    @Min(value = 1, message = "数量不能小于1")
    private Integer quantity;

    // getter + setter
}

命令对象可以通过JSR-380注解(@NotNull、@Min)做参数校验,确保进入应用服务的参数合法,减轻领域层的校验压力。

三、应用服务的测试策略:聚焦流程正确性

应用服务的测试不需要连接真实数据库或外部服务,只需通过Mock框架模拟依赖的领域服务、仓储接口和外部端口,测试流程是否按预期执行。


@SpringBootTest
public class OrderCreationServiceTest {
    // Mock依赖的对象
    @MockBean
    private OrderPriceCalculateService priceService;
    @MockBean
    private OrderRepository orderRepository;
    @MockBean
    private PaymentPort paymentPort;

    // 待测试的应用服务
    @Autowired
    private OrderCreationService orderCreationService;

    @Test
    public void testCreateOrder() {
        // 1. 构造测试参数
        OrderCreateCommand command = new OrderCreateCommand();
        command.setUserId("123");
        // ... 填充其他参数

        // 2. 模拟依赖的返回值
        BigDecimal mockPrice = new BigDecimal("100");
        when(priceService.calculate(any(), any())).thenReturn(mockPrice);
        Order mockOrder = new Order(new OrderId("456"), ...);
        when(orderRepository.save(any())).thenReturn(mockOrder);

        // 3. 执行测试方法
        OrderDTO result = orderCreationService.createOrder(command);

        // 4. 断言结果(流程是否正确执行)
        assertNotNull(result);
        assertEquals("456", result.getOrderId());
        // 验证依赖的方法是否被调用(流程步骤是否完整)
        verify(priceService, times(1)).calculate(any(), any());
        verify(orderRepository, times(1)).save(any());
        verify(paymentPort, times(1)).initPayment(eq("456"), eq(mockPrice));
    }
}

通过这种方式,我们可以快速验证应用服务的流程是否正确,而不受外部依赖的影响,这也是企业级系统“测试驱动开发”的核心实践。

第四篇:CQRS模式:读写分离的企业级实践

“电商大促时,订单查询接口响应慢到超时,但下单流程却很顺畅,这该怎么优化?”——这是高并发场景下的典型问题。原因在于“读写混合”的架构中,大量的查询请求占用了数据库连接,导致写入请求排队。而CQRS模式(命令查询职责分离)通过“读写分离”,让查询和写入各自优化,是解决高并发问题的企业级方案。

一、CQRS的核心:把“读”和“写”彻底分开

CQRS的全称是Command Query Responsibility Segregation,即“命令查询职责分离”,它基于一个简单的原则:

任何一个操作,要么是修改系统状态的“命令”(Command),要么是查询系统状态的“查询”(Query),两者不能同时存在。

在DDD工程落地中,CQRS的具体体现为:

  • 命令端(写端):负责修改数据(如创建订单、取消订单),基于领域模型实现,保证业务规则的正确性,使用事务确保数据一致性。

  • 查询端(读端):负责查询数据(如订单列表、订单详情),不依赖领域模型,直接针对查询场景设计优化的查询模型和SQL,追求查询性能。

举个电商的例子:创建订单(命令端)需要调用领域服务计算价格、校验库存、保存聚合,确保业务规则;而查询订单列表(查询端)只需要从数据库查询“订单ID+用户ID+状态+总价”,不需要任何业务规则,直接返回给前端即可。

二、CQRS在DDD中的工程落地:三大核心组件

CQRS不是“独立架构”,而是融入DDD工程体系的“模式”。基于之前的六边形架构,我们通过“命令处理器”“查询处理器”“读写数据源分离”三个组件实现CQRS。

1. 命令端:基于领域模型的“写流程”

命令端的核心是“确保业务正确性”,复用之前的领域模型、应用服务和仓储模式,无需额外修改。命令端的流程:

  1. 前端提交“创建订单”命令(Command)。

  2. 应用服务接收命令,转换为领域模型。

  3. 调用领域服务和聚合根,执行业务规则。

  4. 通过仓储将数据写入“主数据源”(MySQL主库)。

  5. 发布领域事件(如OrderCreatedEvent),通知查询端更新查询模型。

命令端的代码与之前的应用服务完全一致,核心是通过领域模型保证“写操作”的业务正确性。

2. 查询端:基于查询模型的“读流程”

查询端的核心是“提升查询性能”,因此需要设计独立的查询模型(Read Model),不依赖领域模型,直接对接数据库。查询端的组件:

(1)查询模型(Read Model):为查询场景定制

查询模型是专门为查询场景设计的POJO,只包含查询需要的字段,避免“大字段查询”和“关联查询冗余”。比如订单列表的查询模型:


// 订单列表查询模型(只包含前端需要的字段)
public class OrderListReadModel {
    private String orderId;
    private String userId;
    private String status; // 状态描述,如“待支付”
    private BigDecimal totalPrice;
    private LocalDateTime createTime;

    // getter + setter
}

// 订单详情查询模型(包含更多字段)
public class OrderDetailReadModel {
    private String orderId;
    private String userId;
    private String status;
    private BigDecimal totalPrice;
    private AddressReadModel address; // 地址查询模型
    private List<OrderItemReadModel> items; // 订单项查询模型

    // getter + setter
}
(2)查询处理器(Query Handler):独立的查询逻辑

查询处理器负责接收查询请求,调用查询仓储,返回查询模型。查询处理器不依赖领域层,直接通过MyBatis查询“从数据源”(MySQL从库)。


// 订单查询处理器
@Service
public class OrderQueryHandler {
    private final OrderQueryRepository orderQueryRepository;

    public OrderQueryHandler(OrderQueryRepository orderQueryRepository) {
        this.orderQueryRepository = orderQueryRepository;
    }

    // 处理“查询用户订单列表”查询
    public List<OrderListReadModel> handle(OrderListQuery query) {
        // 直接调用查询仓储,返回查询模型
        return orderQueryRepository.findByUserId(query.getUserId(), query.getStatus());
    }

    // 处理“查询订单详情”查询
    public OrderDetailReadModel handle(OrderDetailQuery query) {
        return orderQueryRepository.findById(query.getOrderId())
                .orElseThrow(() -> new BusinessException("订单不存在"));
    }
}

// 查询参数(与命令对象类似,针对查询场景)
public class OrderListQuery {
    private String userId;
    private String status; // 可选参数,用于筛选
    private Integer pageNum = 1;
    private Integer pageSize = 10;

    // getter + setter
}
(3)查询仓储(Query Repository):优化查询SQL

查询仓储专门用于查询操作,通过优化的SQL(如索引、分页、避免关联)提升性能,对接MySQL从库。


// 订单查询仓储
@Repository
public interface OrderQueryRepository {
    // 优化的分页查询SQL,只查需要的字段
    @Select("SELECT order_id, user_id, status, total_price, create_time " +
            "FROM t_order " +
            "WHERE user_id = #{userId} " +
            "AND (#{status} IS NULL OR status = #{status}) " +
            "ORDER BY create_time DESC " +
            "LIMIT #{pageSize} OFFSET #{(pageNum-1)*pageSize}")
    List<OrderListReadModel> findByUserId(@Param("userId") String userId,
                                          @Param("status") String status,
                                          @Param("pageNum") Integer pageNum,
                                          @Param("pageSize") Integer pageSize);

    // 关联查询订单详情,使用索引优化
    @Select("SELECT o.order_id, o.user_id, o.status, o.total_price, " +
            "o.province, o.city, o.detail, " +
            "i.product_id, i.quantity, i.price " +
            "FROM t_order o " +
            "LEFT JOIN t_order_item i ON o.order_id = i.order_id " +
            "WHERE o.order_id = #{orderId}")
    @Results({
            @Result(column = "order_id", property = "orderId"),
            // 其他字段映射
            @Result(column = "order_id", property = "items",
                    many = @Many(select = "findOrderItemsByOrderId"))
    })
    Optional<OrderDetailReadModel> findById(String orderId);

    @Select("SELECT product_id, quantity, price FROM t_order_item WHERE order_id = #{orderId}")
    List<OrderItemReadModel> findOrderItemsByOrderId(String orderId);
}
3. 读写数据源分离:提升并发能力

CQRS的性能优化核心是“读写数据源分离”:命令端写入MySQL主库,查询端从MySQL从库读取数据,通过数据库的主从复制实现数据同步。在Spring Boot中,通过动态数据源实现:


// 1. 数据源配置
@Configuration
public class DataSourceConfig {
    // 主数据源(写)
    @Bean
    @ConfigurationProperties("spring.datasource.master")
    public DataSource masterDataSource() {
        return DruidDataSourceBuilder.create().build();
    }

    // 从数据源(读)
    @Bean
    @ConfigurationProperties("spring.datasource.slave")
    public DataSource slaveDataSource() {
        return DruidDataSourceBuilder.create().build();
    }

    // 动态数据源(路由)
    @Bean
    public DataSource dynamicDataSource(DataSource masterDataSource, DataSource slaveDataSource) {
        Map&lt;Object, Object&gt; dataSources = new HashMap<>();
        dataSources.put("master", masterDataSource);
        dataSources.put("slave", slaveDataSource);
        DynamicDataSource dynamicDataSource = new DynamicDataSource();
        dynamicDataSource.setTargetDataSources(dataSources);
        dynamicDataSource.setDefaultTargetDataSource(masterDataSource);
        return dynamicDataSource;
    }
}

// 2. 数据源注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface DataSource {
    String value() default "master";
}

// 3. AOP实现数据源切换
@Aspect
@Component
public class DataSourceAspect {
    @Before("@annotation(dataSource)")
    public void before(JoinPoint joinPoint, DataSource dataSource) {
        DynamicDataSourceContextHolder.setDataSource(dataSource.value());
    }

    @After("@annotation(dataSource)")
    public void after(DataSource dataSource) {
        DynamicDataSourceContextHolder.clearDataSource();
    }
}

使用注解标记读写数据源:

// 命令端(写):使用主数据源
@Service
public class OrderCreationService {
    @Transactional
    @DataSource("master")
    public OrderDTO createOrder(OrderCreateCommand command) {
        // ... 写逻辑
    }
}

// 查询端(读):使用从数据源
@Service
public class OrderQueryHandler {
    @DataSource("slave")
    public List<OrderListReadModel> handle(OrderListQuery query) {
        // ... 读逻辑
    }
}

三、企业级优化:CQRS+事件溯源,解决数据一致性问题

CQRS的核心挑战是“读写数据一致性”——命令端写入主库后,主从复制存在延迟,查询端可能查询不到最新数据。企业级解决方案是“CQRS+事件溯源”:

  1. 命令端执行写操作后,发布领域事件(如OrderCreatedEvent)。

  2. 查询端订阅领域事件,在事件处理器中同步更新查询模型(如写入Redis缓存或更新ES索引)。

  3. 查询端优先从缓存/ES查询数据,若未查询到再从数据库查询,确保数据实时性。

以电商订单查询为例,引入ES后的查询流程:


// 1. 命令端发布事件
@Service
public class OrderCreationService {
    private final DomainEventPublisher publisher;

    public OrderDTO createOrder(OrderCreateCommand command) {
        // ... 写逻辑
        // 发布事件
        publisher.publish(new OrderCreatedEvent(savedOrder));
    }
}

// 2. 查询端订阅事件,更新ES
@Component
public class OrderEventProcessor {
    private final RestHighLevelClient esClient;

    @EventListener
    public void handleOrderCreatedEvent(OrderCreatedEvent event) {
        // 将订单数据同步到ES
        OrderDocument document = convertToEsDocument(event.getOrder());
        IndexRequest request = new IndexRequest("order_index").id(document.getOrderId());
        request.source(JSON.toJSONString(document), XContentType.JSON);
        esClient.index(request, RequestOptions.DEFAULT);
    }
}

// 3. 查询端从ES查询
@Service
public class OrderQueryHandler {
    public List<OrderListReadModel> handle(OrderListQuery query) {
        // 从ES查询,性能比数据库高10倍以上
        SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
        BoolQueryBuilder boolQuery = QueryBuilders.boolQuery()
                .must(QueryBuilders.termQuery("userId", query.getUserId()));
        if (query.getStatus() != null) {
            boolQuery.must(QueryBuilders.termQuery("status", query.getStatus()));
        }
        sourceBuilder.query(boolQuery);
        // ... 执行查询并转换为查询模型
    }
}

通过这种方式,查询端的响应时间从原来的300ms+优化到10ms以内,完全满足电商大促的高并发查询需求。

四、CQRS的适用场景与避坑点

CQRS不是“银弹”,需要根据业务场景选择:

  • ✅ 适用场景:高并发查询(如电商订单列表、商品详情)、读写比例失衡(读:写>10:1)、查询场景复杂(需要多表关联或大字段过滤)。

  • ❌ 不适用场景:简单CRUD系统、读写比例均衡(如后台管理系统)、对数据实时性要求极高(如金融交易的实时查询)。

避坑点:不要为了用CQRS而用CQRS!小型系统或简单场景使用CQRS会增加系统复杂度,得不偿失。只有当查询性能成为瓶颈时,才引入CQRS模式。

第三阶段的核心是“将模型转化为可落地的代码”,六边形架构解决了“业务与技术的隔离”,仓储模式解决了“领域与数据库的隔离”,应用服务明确了“流程与规则的边界”,CQRS模式则优化了“高并发场景的性能”。这四篇文章的内容构成了企业级DDD工程落地的核心体系,下一个阶段我们将进入“架构演进”,讲解如何从单体DDD架构平滑迁移

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Coder_Boy_

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值