Во второй части этой серии статей мы реализуем модуль Command
, отвечающий за изменение состояния приложения. Окончательный код находится в Гитхаб .
Мы эффективно начнем кодировать наше приложение с внешнего уровня и продолжим работать внутри. Класс ProductController
будет отвечать за предоставление конечным точкам возможности запрашивать изменения состояния.
Единственной зависимостью этого класса будет зависимость Аксона Командный шлюз
ответственный за отправку команды
объекты. Первоначальная структура будет:
@RestController @RequestMapping("/products") public class ProductController { @Autowired public ProductController(final CommandGateway commandGateway) { this.commandGateway = commandGateway; } private CommandGateway commandGateway; @PostMapping public CompletableFuturecreate(@RequestBody ProductDTO dto) { return null; } @PutMapping public CompletableFuture update(@RequestBody ProductDTO dto) { return null; } }
Где ProductDTO
класс – это просто POJO для отображения запроса json
.
public class ProductDTO { private Long id; private String name; private int quantity; public Long getId() { return id; } public void setId(Long id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public int getQuantity() { return quantity; } public void setQuantity(int quantity) { this.quantity = quantity; } }
Экземпляры команды
в этом приложении будут представлять намерения изменения состояния. Подмножество состояния приложения будет представлено объектом Aggregate
. Например, намерение добавить новый товар в корзину представлено Добавить команду продукта
класс:
public class AddProductCommand { public AddProductCommand( final Long id, final String name, final int quantity) { this.id = id; this.name = name; this.quantity = quantity; } @TargetAggregateIdentifier private final Long id; private final String name; private final int quantity; public Long getId() { return id; } public String getName() { return name; } public int getQuantity() { return quantity; } }
Где Целевой агрегатный идентификатор
аннотация используется для определения того, какой экземпляр Агрегатного
типа должен обрабатываться этой командой.
Теперь, чтобы отправить эту команду из нашего RestController
нам просто нужно создать его экземпляр и передать в качестве аргумента через Командный шлюз
отправить
метод:
@RestController @RequestMapping("/products") public class ProductController { @Autowired public ProductController(final CommandGateway commandGateway) { this.commandGateway = commandGateway; } private CommandGateway commandGateway; @PostMapping public CompletableFuturecreate(@RequestBody ProductDTO dto) { AddProductCommand command = new AddProductCommand( dto.getId(), dto.getName(), dto.getQuantity()); return commandGateway.send(command); } // .... }
Как упоминалось ранее, класс Aggregate
будет отвечать за представление части состояния приложения плюс Команда
обработка для этого Совокупный
.
Примером Агрегата
может быть:
{ "id": 1, "name": "iPhone", "quantity": 2 }
Перевод в код приводит к:
@Aggregate public class ProductAggregate { @AggregateIdentifier private Long id; private String name; private int quantity; @CommandHandler public ProductAggregate(AddProductCommand cmd) { // Verifies state consistency and applies events } }
Обработчик команд
аннотация на конструкторе
означает Команда Добавить продукт
команда используется для создания нового Совокупный
.
На данный момент состояние вашего приложения еще не изменилось. Обработка команд – это место для выполнения бизнес-логики (т. Е.: проверьте, является ли количество положительным значением) и, возможно, примените События
, которые приведут к изменению состояния.
Сначала мы создаем класс AddProductEvent
с атрибутами, необходимыми для запуска желаемого изменения состояния. В данном случае, в частности, он будет очень похож на соответствующую команду
модель.
Теперь мы изменим Совокупность продуктов
|/конструктор для отправки
Добавить событие всякий раз, когда
Отправляется команда Добавить продукт . Этот же класс также может выступать в качестве обработчика источника событий
AddProductEvent , выполняющий изменение состояния приложения.
import static org.axonframework.modelling.command.AggregateLifecycle.apply; @Aggregate public class ProductAggregate { @AggregateIdentifier private Long id; private String name; private int quantity; @CommandHandler public ProductAggregate(AddProductCommand cmd) { apply(new AddProductEvent(cmd.getId(), cmd.getName(), cmd.getQuantity())); } @EventSourcingHandler public void on(AddProductEvent event) { this.id = event.getId(); this.name = event.getName(); this.quantity = event.getQuantity(); } }
До сих пор мы установили контрольную точку, которая позволяет нам тестировать приложение и наблюдать за тем, что происходит. Следуйте командам терминала, чтобы запустить приложение на стороне команды
:
docker-compose up -d ./gradlew clean assemble java -jar commandside/build/libs/commandside.jar
Теперь мы можем протестировать конечную точку, добавив новый продукт:
curl -X POST http://localhost:8080/products -H 'Content-Type: application/json' -d '{"id": 1, "name": "iPhone", "quantity": 7}'
Мы можем проверить в базе данных mongo, в событиях домена
коллекции, что там хранится новое событие:
{ "_id":"5e0a2924b813b63783e1e092", "aggregateIdentifier":"1", "type":"ProductAggregate", "sequenceNumber":"0", "serializedPayload":"", "timestamp":"2019-12-30T16:43:16.851862731Z", "payloadType":"com.example.project.command.addproduct.AddProductEvent", "payloadRevision":null, "serializedMetaData":" 1 iPhone 7 ", "eventIdentifier":"6eef19d8-b22a-4be6-9fd9-7681a31580b8" } traceId e62c8e0d-7505-4e99-ab7e-84b4619ee159 correlationId e62c8e0d-7505-4e99-ab7e-84b4619ee159
Для каждого нового продукта, добавленного с помощью запроса POST, с этого момента у нас будет новая запись в события домена
. Это действует как история того, что произошло в нашем приложении.
Помимо сохранения отдельных событий, мы также хотим сохранять совокупность при каждом изменении (совокупность, хранящаяся в состоянии). Для достижения этой цели нам просто нужно добавить аннотации JPA, чтобы превратить наш совокупный класс в Сущность
:
@Aggregate @Entity // This class can now be mapped to a table public class ProductAggregate { @AggregateIdentifier @Id // Defines the primary key private Long id; @Column // Map to a column with same name private String name; @Column // Map to a column with same name private int quantity; @CommandHandler public ProductAggregate(AddProductCommand cmd) { apply(new AddProductEvent(cmd.getId(), cmd.getName(), cmd.getQuantity())); } @EventSourcingHandler public void on(AddProductEvent event) { this.id = event.getId(); this.name = event.getName(); this.quantity = event.getQuantity(); } }
Теперь добавьте новый продукт на свою карточку, используя конечную точку выше, и проверьте product_table
в своей базе данных Postgres, чтобы проверить, сохранена ли новая запись, соответствующая требуемому агрегату. Ваша база данных Mongo также должна содержать новое событие.
В базе данных Mongo теперь у нас есть история всех событий, которую мы можем использовать, чтобы понять, как приложение достигло своего текущего состояния. С другой стороны, в базе данных Postgres есть данные, которые мы можем использовать для отображения конечным пользователям, например, на экране оформления заказа.
Мы могли бы вернуться назад и реализовать CQR и поиск событий самостоятельно, но, к счастью, мы можем достичь того же результата, используя пару аннотаций от Axon.
Оригинал: “https://dev.to/fabiothiroki/cqrs-using-java-and-axon-command-module-57h5”