CQRSとイベントソーシングのざっくりとした紹介とAxonでの実装について
CQRS(Command Query Responsibility Segregation:コマンドクエリ責務分離)とイベントソーシング
一般的なCRUDアーキテクチャとの比較
典型的なCRUDアーキテクチャでは以下のような形でシステムとの対話します。
- データソースから取得した情報をモデルに反映し、それをもとにUIを表示
- ユーザーがUIを通して情報を変更
- 変更をモデルに反映
- モデルがバリデーションや間接的なロジックを実行
- モデルの変更をデータソースに反映
このアーキテクチャはシンプルでり汎用性も高く、広く一般的に採用されています。
しかし、このアーキテクチャでは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)の初期化などの機能を提供します。
まず、CreateToDoItemCommand
でToDoItem
の新しいインスタンスを作成したいので、@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の完了を通知
}
}