1. 概述

在本文中, 我们将研究Axon如何支持具有多个实体的聚合

我们认为这篇文章是我们关于Axon 的主要指南的扩展。因此,我们将再次使用Axon FrameworkAxon Server 。我们将在本文的代码中使用前者,后者是事件存储和消息路由器。

由于这是一个扩展,让我们详细说明一下我们在基础文章中介绍的 Order 域。

2. 聚合和实体

Axon 支持的聚合和实体源于领域驱动设计。在深入研究代码之前, 我们首先确定此上下文中的实体是什么

  • 本质上 不是 由其属性定义的对象,而是由连续性和同一性的线索定义的对象

因此,一个实体是可识别的,但不能通过它包含的属性来识别。此外,实体会发生变化,因为它保持了连续性。

知道了这一点,我们可以采取以下步骤,分享聚合在此上下文中的含义(摘自领域驱动设计:解决软件核心的复杂性):

  • 聚合是一组关联对象,充当数据更改的单个单元
  • 有关聚合的引用仅限于单个成员,即聚合根
  • 一组一致性规则适用于聚合边界内

正如第一点所示, 聚合不是单个事物,而是一组 对象对象 可以是值对象 ,但更重要的是,它们也可以是实体 。 Axon 支持将聚合建模为一组关联对象而不是单个对象,我们稍后会看到。

3. 订单服务API:命令和事件

当我们处理消息驱动的应用程序时,我们首先在扩展聚合以包含多个实体时定义新命令。

我们的 Order 域当前包含一个 OrderAggregate 。此聚合中包含的一个逻辑概念是 OrderLine 实体。订单行是指正在订购的特定产品,包括产品条目的总数。

知道了这一点,我们可以通过三个附加操作来扩展命令 API(由 PlaceOrderCommandConfirmOrderCommandShipOrderCommand 组成):

  • 添加产品
  • 增加订单行的产品数量
  • 减少订单行的产品数量

这些操作转换为类 AddProductCommandIncrementProductCountCommandDecrementProductCountCommand

public class AddProductCommand {

    @TargetAggregateIdentifier
    private final String orderId;
    private final String productId;

    // default constructor, getters, equals/hashCode and toString
}
 
public class IncrementProductCountCommand {

    @TargetAggregateIdentifier
    private final String orderId;
    private final String productId;

    // default constructor, getters, equals/hashCode and toString
}
 
public class DecrementProductCountCommand {

    @TargetAggregateIdentifier
    private final String orderId;
    private final String productId;

    // default constructor, getters, equals/hashCode and toString
}

TargetAggregateIdentifier 仍然存在于 orderId 上,因为 OrderAggregate 仍然是系统内的聚合。

请记住,从定义来看,实体也有 一个身份 这就是为什么 productId 是命令的一部分。在本文后面,我们将展示这些字段如何引用确切的实体。

事件将作为命令处理的结果发布,通知发生了相关的事情。因此,由于新的命令 API,事件 API 也应该得到扩展。

让我们看看反映增强的 连续性线程的 POJO — ProductAddedEventProductCountIncrementedEventProductCountDecrementedEventProductRemovedEvent

public class ProductAddedEvent {

    private final String orderId;
    private final String productId;

    // default constructor, getters, equals/hashCode and toString
}
 
public class ProductCountIncrementedEvent {

    private final String orderId;
    private final String productId;

    // default constructor, getters, equals/hashCode and toString
}
 
public class ProductCountDecrementedEvent {

    private final String orderId;
    private final String productId;

    // default constructor, getters, equals/hashCode and toString
}
 
public class ProductRemovedEvent {

    private final String orderId;
    private final String productId;

    // default constructor, getters, equals/hashCode and toString
}

4. 聚合和实体:实现

新的 API 规定我们可以添加产品并增加或减少其计数。由于添加到 订单 中的每个产品都会发生这种情况,因此我们需要定义允许这些操作的不同 订单行这表明需要添加属于 OrderAggregate 一部分的 OrderLine 实体。

如果没有指导,Axon 不知道对象是否是聚合中的实体。 我们应该将 AggregateMember 注释放在公开实体的字段或方法上,以将其标记为此类。

我们可以将此注释用于单个对象、对象集合和映射。在 Order 域中,我们最好使用 OrderAggregate 上的 OrderLine 实体的映射

4.1.总体调整

知道了这一点,让我们增强 OrderAggregate

@Aggregate
public class OrderAggregate {

    @AggregateIdentifier
    private String orderId;
    private boolean orderConfirmed;

    @AggregateMember
    private Map<String, OrderLine> orderLines;

    @CommandHandler
    public void handle(AddProductCommand command) {
        if (orderConfirmed) {
            throw new OrderAlreadyConfirmedException(orderId);
        }
        
        String productId = command.getProductId();
        if (orderLines.containsKey(productId)) {
            throw new DuplicateOrderLineException(productId);
        }
        
        AggregateLifecycle.apply(new ProductAddedEvent(orderId, productId));
    }

    // previous command- and event sourcing handlers left out for conciseness

    @EventSourcingHandler
    public void on(OrderPlacedEvent event) {
        this.orderId = event.getOrderId();
        this.orderConfirmed = false;
        this.orderLines = new HashMap<>();
    }

    @EventSourcingHandler
    public void on(ProductAddedEvent event) {
        String productId = event.getProductId();
        this.orderLines.put(productId, new OrderLine(productId));
    }

    @EventSourcingHandler
    public void on(ProductRemovedEvent event) {
        this.orderLines.remove(event.getProductId());
    }
}

使用 AggregateMember 注释标记 orderLines 字段告诉 Axon 它是域模型的一部分。 这样做允许我们在 OrderLine 对象中添加 CommandHandlerEventSourcingHandler 带注释的方法,就像在 Aggregate 中一样。

由于 OrderAggregate 保存 OrderLine 实体, 因此它负责添加和删除产品,以及相应的 OrderLines 该应用程序使用Event Sourcing ,因此有一个 ProductAddedEventProductRemovedEvent EventSourcingHandler 分别用于添加和删除 OrderLine

OrderAggregate 决定何时添加产品或拒绝添加,因为它保存了 OrderLines。 这种所有权表明 AddProductCommand 命令处理程序位于 OrderAggregate 内。

通过发布 ProductAddedEvent 来通知添加成功。如果产品已存在,则抛出 DuplicateOrderLineException ,如果 OrderAggregate 已被确认,则添加不成功;如果 OrderAggregate 已确认,则抛出 OrderAlreadyConfirmedException

最后,我们在 OrderPlacedEvent 处理程序中设置 orderLines 映射 ,因为它是 OrderAggregate 事件流中的第一个事件 。我们可以在 OrderAggregate 或私有构造函数中全局设置该字段,但这意味着状态更改不再是事件源处理程序的唯一域。

4.2.实体介绍

通过更新的 OrderAggregate ,我们可以开始查看 OrderLine

public class OrderLine {

    @EntityId
    private final String productId;
    private Integer count;
    private boolean orderConfirmed;

    public OrderLine(String productId) {
        this.productId = productId;
        this.count = 1;
    }

    @CommandHandler
    public void handle(IncrementProductCountCommand command) {
        if (orderConfirmed) {
            throw new OrderAlreadyConfirmedException(orderId);
        }
        
        apply(new ProductCountIncrementedEvent(command.getOrderId(), productId));
    }

    @CommandHandler
    public void handle(DecrementProductCountCommand command) {
        if (orderConfirmed) {
            throw new OrderAlreadyConfirmedException(orderId);
        }
        
        if (count <= 1) {
            apply(new ProductRemovedEvent(command.getOrderId(), productId));
        } else {
            apply(new ProductCountDecrementedEvent(command.getOrderId(), productId));
        }
    }

    @EventSourcingHandler
    public void on(ProductCountIncrementedEvent event) {
        this.count++;
    }

    @EventSourcingHandler
    public void on(ProductCountDecrementedEvent event) {
        this.count--;
    }

    @EventSourcingHandler
    public void on(OrderConfirmedEvent event) {
        this.orderConfirmed = true;
    }
}

OrderLine 应该是可识别的,如第 2 节中所定义 。该实体可以通过 productId 字段来识别,我们用 EntityId 注释来标记该字段。

使用 EntityId 注释标记字段告诉 Axon 哪个字段标识聚合内的实体实例。

由于 OrderLine 反映了正在订购的产品,因此它负责处理 IncrementProductCountCommandDecrementProductCountCommand 。我们可以在实体内部使用 CommandHandler 注释将这些命令直接路由到适当的实体。

由于使用事件源,需要根据事件设置 OrderLine 的状态。 OrderLine 可以简单地包含设置状态所需的事件的 EventSourcingHandler 注释,类似于 OrderAggregate

使用 EntityId 带注释的字段将命令路由到正确的 OrderLine 实例。为了正确路由, 注释字段的名称应该与命令中包含的字段之一相同 。在此示例中,这由命令和实体中存在的 productId 字段反映。

每当实体存储在集合或映射中时,正确的命令路由都会使 EntityId 成为硬性要求。如果仅定义聚合成员的单个实例,则此要求将放宽为建议。

当命令中的名称与注释字段不同时,我们应该调整 EntityId 注释的 routingKey 值。 routingKey 值应反映命令上的现有字段,以允许命令路由成功。

让我们通过一个例子来解释一下:

public class IncrementProductCountCommand {

    @TargetAggregateIdentifier
    private final String orderId;
    private final String productId;

    // default constructor, getters, equals/hashCode and toString
}

IncrementProductCountCommand 保持不变,包含 orderId 聚合标识符和 ProductId 实体标识符。在 OrderLine 实体中,标识符现在称为 orderLineId

由于 IncrementProductCountCommand 中没有名为 orderLineId 的字段 因此这会破坏基于字段名称的自动命令路由*。*

因此, EntityId 注释上的 routingKey 字段应反映命令中的字段名称,以维持此路由能力。

5. 结论

在本文中,我们了解了聚合包含多个实体的含义以及 Axon Framework 如何支持此概念。

我们增强了订单应用程序,以允许订单行作为单独的实体属于 OrderAggregate

Axon 的聚合建模支持提供 AggregateMember 注释,使用户能够将对象标记为给定聚合的实体。这样做允许命令直接路由到实体,并保持事件源支持。

所有这些示例的实现和代码片段都可以在 GitHub 上找到。

有关此主题的任何其他问题,另请查看讨论 AxonIQ