1.概要

この記事では、 Axon と、 CQRS (コマンドクエリ責任分離)およびイベントソーシングを念頭に置いてアプリケーションを実装するのにどのように役立つかについて説明します。

このガイドでは、AxonFrameworkとAxonServerの両方を利用します。 前者には実装が含まれ、後者は専用のイベントストアおよびメッセージルーティングソリューションになります。

構築するサンプルアプリケーションは、Orderドメインに焦点を当てています。 このために、Axonが提供するCQRSおよびイベントソーシングビルディングブロックを活用します。

共有されている概念の多くは、 DDD、から生まれたものであり、この現在の記事の範囲を超えていることに注意してください。

2. Mavenの依存関係

Axon/Spring Bootアプリケーションを作成します。 したがって、最新のaxon-spring-boot-starter依存関係をpom.xmlに追加する必要があります。また、テスト用にaxon-test依存関係を追加する必要があります。 。 一致するバージョンを使用するには、依存関係管理セクション内でaxon-bomを使用します。

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.axonframework</groupId>
            <artifactId>axon-bom</artifactId>
            <version>4.5.13</version>
        </dependency>
    </dependencies>
</dependencyManagement>

<dependencies>
    <dependency>
        <groupId>org.axonframework</groupId>
        <artifactId>axon-spring-boot-starter</artifactId>
    </dependency>

    <dependency>
        <groupId>org.axonframework</groupId>
        <artifactId>axon-test</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>

3. Axonサーバー

Axon Server を使用して、イベントストアと、専用のコマンド、イベント、およびクエリルーティングソリューションを使用します。

イベントストアとして、イベントを保存するときに必要な理想的な特性を提供します。 このの記事は、これが望ましい理由の背景を提供します。

メッセージルーティングソリューションとして、メッセージを共有およびディスパッチするためのRabbitMQやKafkaトピックなどの構成に焦点を当てることなく、複数のインスタンスを相互に接続するオプションを提供します。

AxonServerはこちらからダウンロードできます。 単純なJARファイルであるため、次の操作で起動できます。

java -jar axonserver.jar

これにより、 localhost:8024からアクセスできる単一のAxonServerインスタンスが起動します。 エンドポイントは、接続されたアプリケーションとそれらが処理できるメッセージの概要、およびAxonServerに含まれるイベントストアへのクエリメカニズムを提供します。

axon-spring-boot-starter 依存関係を伴うAxonServerのデフォルト構成により、Orderサービスが自動的に接続されます。

4. Order Service API –コマンド

CQRSを念頭に置いて注文サービスを設定します。 したがって、アプリケーションを流れるメッセージを強調します。

最初に、意図の表現を意味するコマンドを定義します。 Orderサービスは、3つの異なるタイプのアクションを処理できます。

  1. 新しい注文を作成する
  2. 注文の確認
  3. 注文の発送

当然、ドメインで処理できるコマンドメッセージは CreateOrderCommand ConfirmOrderCommand ShipOrderCommandの3つです。

public class CreateOrderCommand {
 
    @TargetAggregateIdentifier
    private final String orderId;
    private final String productId;
 
    // constructor, getters, equals/hashCode and toString 
}
public class ConfirmOrderCommand {
 
    @TargetAggregateIdentifier
    private final String orderId;
    
    // constructor, getters, equals/hashCode and toString
}
public class ShipOrderCommand {
 
    @TargetAggregateIdentifier
    private final String orderId;
 
    // constructor, getters, equals/hashCode and toString
}

TargetAggregateIdentifierアノテーションは、アノテーションが付けられたフィールドがコマンドのターゲットとなる特定のアグリゲートのIDであることをAxonに通知します。この記事の後半でアグリゲートについて簡単に触れます。

また、コマンドのフィールドをfinalとしてマークしたことに注意してください。これは意図的なものです。メッセージ実装を不変にすることがベストプラクティスであるためです

5. Order Service API –イベント

注文を作成、確認、または発送できるかどうかを決定するのは、アグリゲートがコマンドを処理します。

イベントを公開することにより、アプリケーションの残りの部分にその決定を通知します。 OrderCreatedEvent、OrderConfirmedEvent 、およびOrderShippedEventの3種類のイベントがあります。

public class OrderCreatedEvent {
 
    private final String orderId;
    private final String productId;
 
    // default constructor, getters, equals/hashCode and toString
}
public class OrderConfirmedEvent {
 
    private final String orderId;
 
    // default constructor, getters, equals/hashCode and toString
}
public class OrderShippedEvent { 

    private final String orderId; 

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

6. コマンドモデル–注文集計

コマンドとイベントに関してコアAPIをモデル化したので、コマンドモデルの作成を開始できます。

Aggregate は、コマンドモデル内の通常のコンポーネントであり、DDDに由来します。 他のフレームワークもこの概念を使用しています。たとえば、SpringでのDDDアグリゲートの永続化に関するthisの記事に見られます。

ドメインは注文の処理に重点を置いているため、コマンドモデルの中心としてOrderAggregateを作成します。

6.1. 集約クラス

したがって、基本的な集約クラスを作成しましょう。

@Aggregate
public class OrderAggregate {

    @AggregateIdentifier
    private String orderId;
    private boolean orderConfirmed;

    @CommandHandler
    public OrderAggregate(CreateOrderCommand command) {
        AggregateLifecycle.apply(new OrderCreatedEvent(command.getOrderId(), command.getProductId()));
    }

    @EventSourcingHandler
    public void on(OrderCreatedEvent event) {
        this.orderId = event.getOrderId();
        orderConfirmed = false;
    }

    protected OrderAggregate() { }
}

Aggregateアノテーションは、このクラスをアグリゲートとしてマークするAxonSpring固有のアノテーションです。このOrderAggregateに必要なCQRSおよびイベントソーシング固有のビルディングブロックをインスタンス化する必要があることをフレームワークに通知します。 。

アグリゲートは特定のアグリゲートインスタンスを対象とするコマンドを処理するため、AggregateIdentifierアノテーションを使用して識別子を指定する必要があります。

アグリゲートは、OrderAggregate‘コマンド処理コンストラクター’でCreateOrderCommandを処理するとライフサイクルを開始します。 指定された関数がコマンドを処理できることをフレームワークに伝えるために、CommandHandlerアノテーションを追加します。

CreateOrderCommandを処理するとき、OrderCreatedEventを公開することにより、注文が作成されたことをアプリケーションの残りの部分に通知します。 アグリゲート内からイベントを公開するには、次を使用します AggregateLifecycle#apply(Object…)

この時点から、イベントのストリームから集約インスタンスを再作成するための原動力として、実際にイベントソーシングを組み込むことができます。

これは、「集約作成イベント」である OrderCreatedEvent から開始します。これは、 EventSourcingHandler アノテーション付き関数で処理され、orderIdおよびorderConfirmedを設定します。 注文集計の状態。

また、イベントに基づいてアグリゲートを調達できるようにするには、Axonにデフォルトのコンストラクターが必要であることに注意してください。

6.2. 集約コマンドハンドラー

基本的な集計ができたので、残りのコマンドハンドラーの実装を開始できます。

@CommandHandler 
public void handle(ConfirmOrderCommand command) { 
    if (orderConfirmed) {
        return;
    }
    apply(new OrderConfirmedEvent(orderId)); 
} 

@CommandHandler 
public void handle(ShipOrderCommand command) { 
    if (!orderConfirmed) { 
        throw new UnconfirmedOrderException(); 
    } 
    apply(new OrderShippedEvent(orderId)); 
} 

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

コマンドおよびイベントソーシングハンドラーの署名は、簡潔な形式を維持するために、 handle({the-command})および on({the-event})を単純に示します。

さらに、注文は1回だけ確認でき、確認された場合は発送できると定義しています。 したがって、前者のコマンドは無視し、後者が当てはまらない場合はUnconfirmedOrderExceptionをスローします。

これは、OrderConfirmedEventソーシングハンドラーがOrderアグリゲートのorderConfirmed状態をtrueに更新する必要があることを示しています。

7. コマンドモデルのテスト

まず、OrderAggregateFixtureConfigurationを作成してテストを設定する必要があります。

private FixtureConfiguration<OrderAggregate> fixture;

@Before
public void setUp() {
    fixture = new AggregateTestFixture<>(OrderAggregate.class);
}

最初のテストケースは、最も単純な状況をカバーする必要があります。 アグリゲートがCreateOrderCommandを処理する場合、OrderCreatedEventを生成する必要があります。

String orderId = UUID.randomUUID().toString();
String productId = "Deluxe Chair";
fixture.givenNoPriorActivity()
  .when(new CreateOrderCommand(orderId, productId))
  .expectEvents(new OrderCreatedEvent(orderId, productId));

次に、注文が確認された場合にのみ発送できるという意思決定ロジックをテストできます。 このため、2つのシナリオがあります。1つは例外を予期するシナリオで、もう1つはOrderShippedEventを予期するシナリオです。

例外が予想される最初のシナリオを見てみましょう。

String orderId = UUID.randomUUID().toString();
String productId = "Deluxe Chair";
fixture.given(new OrderCreatedEvent(orderId, productId))
  .when(new ShipOrderCommand(orderId))
  .expectException(UnconfirmedOrderException.class);

次に、 OrderShippedEventを期待する2番目のシナリオ:

String orderId = UUID.randomUUID().toString();
String productId = "Deluxe Chair";
fixture.given(new OrderCreatedEvent(orderId, productId), new OrderConfirmedEvent(orderId))
  .when(new ShipOrderCommand(orderId))
  .expectEvents(new OrderShippedEvent(orderId));

8. クエリモデル–イベントハンドラ

これまでのところ、コマンドとイベントを使用してコアAPIを確立し、CQRSOrderサービスのコマンドモデルであるOrderAggregateを配置しています。

次に、アプリケーションがサービスする必要があるクエリモデルの1つについて考え始めることができます。

これらのモデルの1つは、Orderです。

public class Order {

    private final String orderId;
    private final String productId;
    private OrderStatus orderStatus;

    public Order(String orderId, String productId) {
        this.orderId = orderId;
        this.productId = productId;
        orderStatus = OrderStatus.CREATED;
    }

    public void setOrderConfirmed() {
        this.orderStatus = OrderStatus.CONFIRMED;
    }

    public void setOrderShipped() {
        this.orderStatus = OrderStatus.SHIPPED;
    }

    // getters, equals/hashCode and toString functions
}
public enum OrderStatus {
    CREATED, CONFIRMED, SHIPPED
}

システムを介して伝播するイベントに基づいてこのモデルを更新します。モデルを更新するSpringService Beanは、次のトリックを実行します。

@Service
public class OrdersEventHandler {

    private final Map<String, Order> orders = new HashMap<>();

    @EventHandler
    public void on(OrderCreatedEvent event) {
        String orderId = event.getOrderId();
        orders.put(orderId, new Order(orderId, event.getProductId()));
    }

    // Event Handlers for OrderConfirmedEvent and OrderShippedEvent...
}

axon-spring-boot-starter 依存関係を使用してAxonアプリケーションを開始したため、フレームワークはすべてのBeanを自動的にスキャンして既存のメッセージ処理関数を探します。

OrdersEventHandlerにはEventHandlerの注釈付き関数があり、 Order を格納して更新するため、このBeanは、必要なしにイベントを受信する必要があるクラスとしてフレームワークによって登録されます。私たちの側の構成。

9. クエリモデル–クエリハンドラ

次に、このモデルをクエリして、たとえばすべての注文を取得するには、最初にコアAPIにクエリメッセージを導入する必要があります。

public class FindAllOrderedProductsQuery { }

次に、 FindAllOrderedProductsQuery を処理できるように、OrdersEventHandlerを更新する必要があります。

@QueryHandler
public List<Order> handle(FindAllOrderedProductsQuery query) {
    return new ArrayList<>(orders.values());
}

The QueryHandler 注釈付き関数は FindAllOrderedProductsQuery を返すように設定されていますリストとにかく、他の「すべて検索」クエリと同様です。

10. すべてをまとめる

コマンド、イベント、クエリを使用してコアAPIを具体化し、OrderAggregateモデルとOrderモデルを使用してコマンドとクエリモデルを設定しました。

次は、インフラストラクチャのルーズエンドを結び付けることです。 axon-spring-boot-starter を使用しているため、これにより、必要な構成の多くが自動的に設定されます。

初め、 アグリゲートにイベントソーシングを活用したいので、EventStoreが必要になります。 手順3で起動したAxonServerがこの穴を埋めます。

次に、Orderクエリモデルを保存するメカニズムが必要です。 この例では、 h2 をインメモリデータベースとして追加し、spring-boot-starter-data-jpaを追加して使いやすくすることができます。

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
    <scope>runtime</scope>
</dependency>

10.1. RESTエンドポイントの設定

次に、 spring-boot-starter-web 依存関係を追加して、RESTエンドポイントを活用するアプリケーションにアクセスできるようにする必要があります。

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

RESTエンドポイントから、コマンドとクエリのディスパッチを開始できます。

@RestController
public class OrderRestEndpoint {

    private final CommandGateway commandGateway;
    private final QueryGateway queryGateway;

    // Autowiring constructor and POST/GET endpoints
}

CommandGatewayはコマンドメッセージを送信するメカニズムとして使用され、QueryGatewayはクエリメッセージを送信します。ゲートウェイは、 CommandBus と比較して、よりシンプルでわかりやすいAPIを提供します。および接続先のQueryBus

これ以降、 OrderRestEndpointには、注文を作成、確認、発送するためのPOSTエンドポイントが必要です

@PostMapping("/ship-order")
public CompletableFuture<Void> shipOrder() {
    String orderId = UUID.randomUUID().toString();
    return commandGateway.send(new CreateOrderCommand(orderId, "Deluxe Chair"))
                         .thenCompose(result -> commandGateway.send(new ConfirmOrderCommand(orderId)))
                         .thenCompose(result -> commandGateway.send(new ShipOrderCommand(orderId)));
}

これにより、CQRSアプリケーションのコマンド側が切り上げられます。 CompletableFutureがゲートウェイによって返され、非同期が有効になることに注意してください。

これで、残っているのは、すべての Order:を照会するためのGETエンドポイントだけです。

@GetMapping("/all-orders")
public CompletableFuture<List<Order>> findAllOrders() {
    return queryGateway.query(new FindAllOrderedProductsQuery(), ResponseTypes.multipleInstancesOf(Order.class));
}

GETエンドポイントでは、QueryGatewayを利用してポイントツーポイントクエリをディスパッチします。その際、デフォルトの FindAllOrderedProductsQuery を作成しますが、期待収益も指定する必要があります。タイプ。

複数のOrderインスタンスが返されることが予想されるため、静的 ResponseTypes#multipleInstancesOf(Class)関数を利用します。 これにより、注文サービスのクエリ側への基本的な入り口が提供されました。

セットアップが完了したので、 OrderApplication。を起動したら、RESTコントローラーを介していくつかのコマンドとクエリを送信できます。

エンドポイント/ship-order にPOSTすると、 OrderAggregate がインスタンス化され、イベントが公開されます。これにより、Ordersが保存/更新されます。GET – / all-orders エンドポイントからのingは、 OrdersEventHandler によって処理されるクエリメッセージを公開します。これにより、既存のすべてのOrdersが返されます。

11. 結論

この記事では、CQRSとイベントソーシングの利点を活用するアプリケーションを構築するための強力なベースとしてAxonフレームワークを紹介しました。

このようなアプリケーションを実際にどのように構成するかを示すために、フレームワークを使用して単純なOrderサービスを実装しました。

最後に、Axon Serverはイベントストアとメッセージルーティングメカニズムとして機能し、インフラストラクチャを大幅に簡素化しました。

これらすべての例とコードスニペットの実装は、GitHubにあります。

このトピックに関するその他の質問については、 DiscussAxonIQも確認してください。