1. 概述
在本文中, 我们将研究Axon如何支持具有多个实体的聚合 。
我们认为这篇文章是我们关于Axon 的主要指南的扩展。因此,我们将再次使用Axon Framework和Axon Server 。我们将在本文的代码中使用前者,后者是事件存储和消息路由器。
由于这是一个扩展,让我们详细说明一下我们在基础文章中介绍的 Order 域。
2. 聚合和实体
Axon 支持的聚合和实体源于领域驱动设计。在深入研究代码之前, 我们首先确定此上下文中的实体是什么 :
- 本质上 不是 由其属性定义的对象,而是由连续性和同一性的线索定义的对象
因此,一个实体是可识别的,但不能通过它包含的属性来识别。此外,实体会发生变化,因为它保持了连续性。
知道了这一点,我们可以采取以下步骤,分享聚合在此上下文中的含义(摘自领域驱动设计:解决软件核心的复杂性):
- 聚合是一组关联对象,充当数据更改的单个单元
- 有关聚合的引用仅限于单个成员,即聚合根
- 一组一致性规则适用于聚合边界内
正如第一点所示, 聚合不是单个事物,而是一组 对象 。 对象 可以是值对象 ,但更重要的是,它们也可以是实体 。 Axon 支持将聚合建模为一组关联对象而不是单个对象,我们稍后会看到。
3. 订单服务API:命令和事件
当我们处理消息驱动的应用程序时,我们首先在扩展聚合以包含多个实体时定义新命令。
我们的 Order 域当前包含一个 OrderAggregate 。此聚合中包含的一个逻辑概念是 OrderLine 实体。订单行是指正在订购的特定产品,包括产品条目的总数。
知道了这一点,我们可以通过三个附加操作来扩展命令 API(由 PlaceOrderCommand 、 ConfirmOrderCommand 和 ShipOrderCommand 组成):
- 添加产品
- 增加订单行的产品数量
- 减少订单行的产品数量
这些操作转换为类 AddProductCommand 、 IncrementProductCountCommand 和 DecrementProductCountCommand :
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 — ProductAddedEvent 、 ProductCountIncrementedEvent 、 ProductCountDecrementedEvent 和 ProductRemovedEvent :
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 对象中添加 CommandHandler 和 EventSourcingHandler 带注释的方法,就像在 Aggregate 中一样。
由于 OrderAggregate 保存 OrderLine 实体, 因此它负责添加和删除产品,以及相应的 OrderLines 。 该应用程序使用Event Sourcing ,因此有一个 ProductAddedEvent 和 ProductRemovedEvent 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 反映了正在订购的产品,因此它负责处理 IncrementProductCountCommand 和 DecrementProductCountCommand 。我们可以在实体内部使用 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 。