CQRS+イベントソーシングフレームワーク Axon


CQRSとイベントソーシングのざっくりとした紹介とAxonでの実装について

CQRS(Command Query Responsibility Segregation:コマンドクエリ責務分離)とイベントソーシング

一般的なCRUDアーキテクチャとの比較

典型的なCRUDアーキテクチャでは以下のような形でシステムとの対話します。

  1. データソースから取得した情報をモデルに反映し、それをもとにUIを表示
  2. ユーザーがUIを通して情報を変更
  3. 変更をモデルに反映
  4. モデルがバリデーションや間接的なロジックを実行
  5. モデルの変更をデータソースに反映

このアーキテクチャはシンプルでり汎用性も高く、広く一般的に採用されています。
しかし、このアーキテクチャではDTOの送受信によるデータ中心の対話になるためドメインモデルの振る舞いが上手く表現できません。

CQRSとイベントソーシング

CQRSとイベントソーシングを適用すると以下のような形をとれます。

CQRSを適用し更新のモデルと参照のモデルを明確にわけることで、データ中心の対話ではなく、ユーザーの意図をコマンドとして伝えることができます。
例えば、CRUDアーキテクチャでは「ユーザー情報を更新する」としか表現できなかったものが、「ユーザーの住所を変更する」コマンドを発行するというようにより明確な意図が表現できます。
また、もっと言えば住所変更が入力ミスの修正なのか、引っ越しによるものなのかといったことも表現できるようになります。ドメインモデルはコマンドを処理し、イベントを生成する事でどのように振る舞うかを表現できます。
またイベントソーシングを適用することで、CRUDのように「何かが起きた結果の状態」を保存するのではなく「何が起きたか(イベント)自体」を保存しすることにより現在の状態になった経緯や理由も保持できるとともに、イベントを介してコンポーネントを緩やかに結合できるため機能の拡張性にも優れます。

Axon

AxonはCQRSとイベントソーシングに基づいたフレームワークです。今回紹介するバージョンは2.4.5です。
Axonのアーキテクチャは下図のようになります。

アプリケーションに対する状態変化はCommandから始まります。
CommandHandlerによって処理されドメインオブジェクト(Aggregate)の状態を変更します。
そしてドメインオブジェクトの状態変更によりドメインイベントが発生します。
ドメインオブジェクトで発生したイベントはリポジトリを通してイベントストアに永続化します。
またEventはEventBusを通じてディスパッチされ、イベントハンドラによってクエリに使用するデータソースを更新したり、外部システムにメッセージを送信したります。

ToDoの登録と完了マークをつけるだけのシンプルなアプリケーションを例にAxonでどのように実装するかみてみましょう。

CommandとEvent

Commandはアプリケーションに対する意図を表しその意図に基づいて処理するときに必要なデータを持ったオブジェクトです。
Eventはアプリケーション内で何が発生したかを表現するオブジェクトです。
Todo作成のCommandとイベントは以下のようになります。

public class CreateToDoItemCommand {

    @TargetAggregateIdentifier
    private final String todoId;
    private final String description;

    public CreateToDoItemCommand(String todoId, String description) {
        this.todoId = todoId;
        this.description = description;
    }

    public String getTodoId() {
        return todoId;
    }

    public String getDescription() {
        return description;
    }
}
public class ToDoItemCreatedEvent {

    private final String todoId;
    private final String description;

    public ToDoItemCreatedEvent(String todoId, String description) {
        this.todoId = todoId;
        this.description = description;
    }

    public String getTodoId() {
        return todoId;
    }

    public String getDescription() {
        return description;
    }
}

@TargetAggregateIdentifierはターゲットとなるAggregateインスタンスを識別するのに使用するフィールド(またはメソッド)を示します。
同様に完了のマークをするCommandとEventをつくります。

public class MarkCompletedCommand {

    @TargetAggregateIdentifier
    private final String todoId;

    public MarkCompletedCommand(String todoId) {
        this.todoId = todoId;
    }

    public String getTodoId() {
        return todoId;
    }
}
public class ToDoItemCompletedEvent {

    private final String todoId;

    public ToDoItemCompletedEvent(String todoId) {
        this.todoId = todoId;
    }

    public String getTodoId() {
        return todoId;
    }
}

ドメインモデル

AxonでのドメインモデルはCommandを受けて状態を変更し、それに対するEventを発行するAggregateして振る舞います。
ToDoを表すToDoItemを実装すると以下のようになります。

public class ToDoItem extends AbstractAnnotatedAggregateRoot {

    @AggregateIdentifier
    private String id;
    private String description;
    private boolean completed;

    public ToDoItem() {
    }

    @CommandHandler
    public ToDoItem(CreateToDoItemCommand command) {
        apply(new ToDoItemCreatedEvent(command.getTodoId(), command.getDescription()));
    }


    @CommandHandler
    public void markCompleted(MarkCompletedCommand command) {
       apply(new ToDoItemCompletedEvent(id));
     }

    @EventSourcingHandler
    public void on(ToDoItemCreatedEvent event) {
        this.id = event.getTodoId();
        this.desc = event.getDescription();
    }

    @EventSourcingHandler
    public void on(ToDoItemCompletedEvent event) {
        this.completed = true;
    }
}

AbstractAnnotatedAggregateRootはイベントの永続化やEventBusへのディスパッチ、イベントストアから取得したイベントストリームをもとにドメインオブジェクト(Aggregate)の初期化などの機能を提供します。

まず、CreateToDoItemCommandToDoItemの新しいインスタンスを作成したいので、@CommandHandler
を付けたコンストラクタを作成します。コンストラクタでapply()を呼ぶことでToDoItemCreatedEventが発行されイベントストアに永続化されます。また発生したイベントがEventBusを通してToDoItemCreatedEventに関心のあるイベントリスナーにディスパッチされます。
同様に完了マークを付けた場合にはToDoItemCompletedEventを発生させたいのでmarkCompletedメソッドを作成します。
MarkCompletedCommandが発行された場合は、イベントストアから読み込まれたイベントストリームが適用されて値が設定されたToDoItemインスタンスに対してmarkCompleted()が呼ばれます。

またToDoItemインスタンスを生成する際にインスタンスの状態を初期化するために@EventSourcingHandlerをつけたイベントハンドラを作成します。

イベントリスナー

イベントリスナーを作成することで簡単にEventに対するアクションが行なえます。
例えば以下のようにToDoItemの現在の状態を参照用DBに書き込んだり

public class ToDoEventListener {

    @EventHandler
    public void handle(ToDoItemCreatedEvent event) {
        //参照用DBの更新
    }

    @EventHandler
    public void handle(ToDoItemCompletedEvent event) {
        //参照用DBの更新
    }
}

以下のように完了通知を行う機能を追加することもできます。

public class ToDoEventNotifyListener {
    @EventHandler
    public void handle(ToDoItemCompletedEvent event) {
        //ToDoの完了を通知
    }
}