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» .