Перейти к основному содержимому

Агрегаты с несколькими сущностями в Axon

· 8 мин. чтения

1. Обзор

В этой статье мы рассмотрим, как Axon поддерживает агрегаты с несколькими сущностями .

Мы считаем эту статью расширением нашего основного руководства по Axon . Таким образом, мы снова будем использовать и Axon Framework , и Axon Server . Мы будем использовать первое в коде этой статьи, а второе — хранилище событий и маршрутизатор сообщений.

Поскольку это расширение, давайте немного остановимся на домене Order , который мы представили в основной статье.

2. Агрегаты и сущности

Агрегаты и объекты, которые поддерживает Axon, основаны на проектировании, управляемом предметной областью . Прежде чем погрузиться в код, давайте сначала установим, что представляет собой сущность в этом контексте:

  • Объект, который в своей основе определяется не своими атрибутами, а скорее нитью непрерывности и идентичности .

Таким образом, сущность идентифицируема, но не через содержащиеся в ней атрибуты. Кроме того, изменения происходят в сущности, поскольку она поддерживает нить непрерывности.

Зная это, мы можем сделать следующий шаг, поделившись тем, что означает Aggregate в этом контексте (отобрано из Domain-Driven Design: Tackling Complexity in the Heart of Software ):

  • Агрегат — это группа связанных объектов, действующих как единое целое для изменения данных.
  • Ссылки на Агрегат ограничены одним членом, Корнем Агрегата.
  • Набор правил согласованности применяется в пределах границы агрегата.

Как следует из первого пункта, Aggregate — это не отдельная вещь, а группа объектов . Объекты могут быть объектами-значениями , но, что более важно, они также могут быть сущностями . 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 требует, чтобы мы могли добавлять продукт и увеличивать или уменьшать его количество. Поскольку это происходит для каждого продукта, добавленного в Order , нам необходимо определить отдельные строки заказа, разрешающие эти операции. Это сигнализирует о необходимости добавить сущность OrderLine , которая является частью OrderAggregate .

Аксон не знает без руководства, является ли объект сущностью в Агрегате. Мы должны поместить аннотацию AggregateMember в поле или метод, раскрывающий объект, чтобы пометить его как таковой.

Мы можем использовать эту аннотацию для отдельных объектов, коллекций объектов и карт. В домене Order нам лучше использовать карту объекта OrderLine в OrderAggregate.

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());
}
}

Пометка поля orderLines аннотацией AggregateMember сообщает Axon, что это часть модели предметной области. Это позволяет нам добавлять аннотированные методы CommandHandler и EventSourcingHandler в объект OrderLine , как и в Aggregate.

Поскольку OrderAggregate содержит сущности OrderLine , он отвечает за добавление и удаление продуктов и, следовательно, за соответствующие OrderLines . Приложение использует Event Sourcing , поэтому есть ProductAddedEvent и ProductRemovedEvent EventSourcingHandler , которые соответственно добавляют и удаляют OrderLine .

OrderAggregate решает , когда добавить продукт или отклонить добавление, поскольку он содержит OrderLines. Это владение требует, чтобы обработчик команды AddProductCommand находился внутри OrderAggregate .

Об успешном добавлении сообщается посредством публикации события ProductAddedEvent . Неудачное добавление следует из выбрасывания DuplicateOrderLineException , если продукт уже присутствует, и OrderAlreadyConfirmedException , если OrderAggregate уже подтвержден.

Наконец, мы устанавливаем карту orderLines в обработчике OrderPlacedEvent , поскольку это первое событие в потоке событий 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 внутри объекта, чтобы напрямую направлять эти команды в соответствующий объект.

Поскольку используется Event Sourcing, состояние OrderLine необходимо устанавливать на основе событий. OrderLine может просто включать аннотацию EventSourcingHandler для событий, необходимых для установки состояния, подобно OrderAggregate .

Направление команды в правильный экземпляр OrderLine выполняется с помощью аннотированного поля EntityId . Для правильной маршрутизации имя аннотируемого поля должно совпадать с одним из полей, содержащихся в команде . В этом примере это отражается в поле productId, присутствующем в командах и в сущности.

Правильная маршрутизация команд делает EntityId жестким требованием всякий раз, когда объект хранится в коллекции или на карте. Это требование превращается в рекомендацию, если определен только один экземпляр составного элемента.

Мы должны корректировать значение routingKey аннотации EntityId всякий раз, когда имя в команде отличается от аннотированного поля. Значение routingKey должно отражать существующее поле в команде, чтобы обеспечить успешную маршрутизацию команды.

Поясним это на примере:

public class IncrementProductCountCommand {

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

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

@EntityId(routingKey = "productId")
private final String orderLineId;
private Integer count;
private boolean orderConfirmed;

// constructor, command and event sourcing handlers
}

Команда IncrementProductCountCommand осталась прежней и содержит совокупный идентификатор orderId и идентификатор объекта productId . В сущности OrderLine идентификатор теперь называется orderLineId .

Поскольку в IncrementProductCountCommand нет поля с именем orderLineId , это нарушит автоматическую маршрутизацию команд на основе имени поля .

Следовательно, поле routingKey в аннотации EntityId должно отражать имя поля в команде, чтобы поддерживать эту возможность маршрутизации.

5. Вывод

В этой статье мы рассмотрели, что означает, что агрегат может содержать несколько сущностей, и как Axon Framework поддерживает эту концепцию.

Мы усовершенствовали приложение Order, чтобы позволить строкам Order как отдельным объектам принадлежать OrderAggregate .

Поддержка моделирования агрегатов Axon предоставляет аннотацию AggregateMember , позволяющую пользователям отмечать объекты как сущности данного агрегата. Это позволяет направлять команды непосредственно к объекту, а также поддерживать поддержку источников событий.

Реализацию всех этих примеров и фрагментов кода можно найти на GitHub .

Если у вас возникнут дополнительные вопросы по этой теме, также посетите страницу «Обсудить AxonIQ» .