1. 概要

この記事では、Axonが複数のエンティティを持つアグリゲートをどのようにサポートするかを調べます

この記事は、Axonに関するメインガイドの拡張版であると考えています。 そのため、 AxonFrameworkAxonServerの両方を再び利用します。 この記事のコードでは前者を使用し、後者はイベントストアとメッセージルーターです。

これは拡張であるため、基本記事で示したOrderドメインについて少し詳しく説明します。

2. 骨材とエンティティ

Axonがサポートするアグリゲートとエンティティは、ドメイン駆動設計に由来します。 コードに飛び込む前に、まず、このコンテキスト内でエンティティが何であるかを確立しましょう。

  • 基本的にその属性によってではなく定義されているオブジェクトであり、継続性とアイデンティティのスレッドによって定義されているオブジェクト

したがって、エンティティは識別可能ですが、含まれる属性を介しては識別できません。 さらに、エンティティは継続性のスレッドを維持するため、変更が発生します。

これを知っていると、このコンテキストでアグリゲートが何を意味するかを共有することで、次のステップに進むことができます(ドメイン駆動設計:ソフトウェアの中心にある複雑さへの取り組み):

  • 集約は、データ変更に対して単一のユニットとして機能する関連オブジェクトのグループです。
  • アグリゲートに関する参照は、単一のメンバーであるアグリゲートルートに制限されています。
  • 一連の整合性ルールが集計境界内に適用されます

最初のポイントが示すように、集合体は単一のものではなく、オブジェクトのグループです。 オブジェクト値オブジェクトにすることができますが、さらに重要なことに、エンティティにすることもできます。 Axonは、後で説明するように、単一のオブジェクトではなく、関連付けられたオブジェクトのグループとしての集合体のモデリングをサポートしています。

3. Order Service API:コマンドとイベント

メッセージ駆動型アプリケーションを扱っているので、複数のエンティティを含むようにAggregateを拡張するときに、新しいコマンドを定義することから始めます。

現在、OrderドメインにはOrderAggregateが含まれています。 この集計に含める論理的な概念は、OrderLineエンティティです。 注文明細は、製品エントリの総数を含む、注文されている特定の製品を指します。

これを知っていると、 PlaceOrderCommand ConfirmOrderCommand、 ShipOrderCommand で構成されるコマンドAPIを、次の3つの追加操作で拡張できます。

  • 製品の追加
  • 注文ラインの製品数を増やす
  • 注文ラインの製品数を減らす

これらの操作は、クラス 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
}

OrderAggregate はシステム内の集約のままであるため、TargetAggregateIdentifierorderIdに引き続き存在します。

定義から、エンティティにはIDもあることを思い出してください。これが、productIdがコマンドの一部である理由です。 この記事の後半で、これらのフィールドが正確なエンティティをどのように参照するかを示します。

コマンド処理の結果としてイベントが公開され、関連する何かが発生したことが通知されます。 したがって、新しいコマンドAPIの結果として、イベントAPIも拡張する必要があります。

拡張された連続性のスレッド ProductAddedEvent ProductCountIncrementedEvent ProductCountDecrementedEvent 、およびProductRemovedEventを反映するPOJOを見てみましょう。 ]:

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は、ガイダンスなしでは、オブジェクトがAggregate内のエンティティであるかどうかを知りません。 AggregateMemberアノテーションをフィールドまたはメソッドに配置して、エンティティを公開し、そのようにマークする必要があります。

この注釈は、単一のオブジェクト、オブジェクトのコレクション、およびマップに使用できます。 の中に注文ドメイン、私たちはマップを使用する方が良いです 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にドメインモデルの一部であることを通知します。 これを行うと、Aggregateの場合と同様に、CommandHandlerおよびEventSourcingHandlerの注釈付きメソッドをOrderLineオブジェクトに追加できます。

OrderAggregateOrderLineエンティティを保持しているため、は製品、つまりそれぞれのOrderLineの追加と削除を担当します。アプリケーションはイベントを使用しますSourcing なので、ProductAddedEventProductRemovedEvent EventSourcingHandler があり、それぞれOrderLineを追加および削除します。

The OrderAggregate 製品を追加するか、追加を拒否するかを決定します。 OrderLines。 この所有権は、 AddProductCommand コマンドハンドラは、 OrderAggregate

追加が成功すると、ProductAddedEventが公開されて通知されます。 追加が失敗したのは、製品がすでに存在する場合は DuplicateOrderLineException をスローし、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;
    }
}

セクション2で定義されているように、OrderLineは識別可能である必要があります。 エンティティは、EntityIdアノテーションでマークしたproductIdフィールドで識別できます。

EntityId アノテーションでフィールドをマークすると、どのフィールドがアグリゲート内のエンティティインスタンスを識別するかがAxonに通知されます。

OrderLine は注文されている商品を反映しているため、IncrementProductCountCommandおよびDecrementProductCountCommandの処理を担当します。 エンティティ内でCommandHandlerアノテーションを使用して、これらのコマンドを適切なエンティティに直接ルーティングできます。

イベントソーシングを使用するため、OrderLineの状態をイベントに基づいて設定する必要があります。 OrderLine には、 OrderAggregate と同様に、状態を設定するために必要なイベントのEventSourcingHandlerアノテーションを含めることができます。

コマンドを正しいOrderLineインスタンスにルーティングするには、EntityId注釈付きフィールドを使用します。 正しくルーティングするには、注釈付きフィールドの名前は、コマンドに含まれるフィールドの1つと同じである必要があります。 このサンプルでは、これはコマンドとエンティティに存在するproductIdフィールドに反映されています。

正しいコマンドルーティングにより、エンティティがコレクションまたはマップに格納されている場合は常に、EntityIdが厳しい要件になります。 集約メンバーのインスタンスが1つだけ定義されている場合、この要件は推奨事項に緩和されます。

コマンドの名前が注釈付きフィールドと異なる場合は常に、EntityId注釈のroutingKey値を調整する必要があります。 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と呼ばれるようになりました。

と呼ばれるフィールドがないので orderLineId の中に IncrementProductCountCommand、 これにより、フィールド名に基づく自動コマンドルーティングが中断されます

したがって、EntityIdアノテーションのroutingKeyフィールドは、このルーティング機能を維持するために、コマンドのフィールド名を反映する必要があります。 

5. 結論

この記事では、アグリゲートに複数のエンティティが含まれることの意味と、AxonFrameworkがこの概念をどのようにサポートするかについて説明しました。

Orderアプリケーションが拡張され、個別のエンティティとしてのOrderLinesがOrderAggregateに属することができるようになりました。

Axonのアグリゲートモデリングサポートは、 AggregateMember アノテーションを提供し、ユーザーがオブジェクトを特定のアグリゲートのエンティティとしてマークできるようにします。 そうすることで、エンティティへのコマンドルーティングを直接実行できるだけでなく、イベントソーシングのサポートを維持できます。

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

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