最近、 Zinc Prodポッドキャストで、私の友人と私はCQRS / ESパターンとElixirでのその実装のいくつかの機能について議論しました。 なぜなら 私は自分の仕事でLaravelを使用しています。インターネットを掘り下げず、このフレームワークのエコシステムでこのアプローチをすすめる方法を見つけられないのは罪でした。
私は皆、カットの下に招待します、私はトピックをできるだけ抽象的に説明しようとしました。
いくつかの定義
CQRS (Command Query Responsibility Segregation)-読み取り操作と書き込み操作を別々のエンティティに割り当てます。 たとえば、マスターに書き込み、レプリカから読み取ります。 CQRS。 事実と誤解 -Zen CQRSを完全に理解するのに役立ちます。
ES (イベントソーシング)-エンティティまたはエンティティセットのすべての状態変化のストレージ。
CQRS / ESは、エンティティの状態変化のすべてのイベントをイベントテーブルに保存し、これに集約とプロジェクターを追加するアーキテクチャアプローチです。
集約 -ビジネスロジックの決定(書き込みを高速化するため)に必要なプロパティをメモリに格納し、決定(ビジネスロジック)を行い、イベントを発行します。
プロジェクター -イベントをリッスンし、個別のテーブルまたはデータベースに書き込みます(読み取りを高速化するため)。
戦いに
Laravelイベントプロジェクター -Laravel 用 CQRS / ESライブラリ
Larabankは、CQRS / ESアプローチを備えたリポジトリです。 試用します。
ライブラリ構成は、どこを調べて何であるかを教えてくれます。 event-projector.phpファイルを確認します。 仕事を説明するのに必要なもの:
-
projectors
-projectors
登録します。 -
reactors
-reactors
登録します。 Reactor-このライブラリでは、イベント処理に副作用を追加します。たとえば、このリポジトリでは、撤回の制限を3回超えようとすると、 MoreMoneyNeededイベントが書き込まれ、財政難に関するメッセージがユーザーに送信されます。 -
replay_chunk_size
は、繰り返しチャンクのサイズです。 ESの機能の1つは、イベントから履歴を復元する機能です。 Laravelイベントプロジェクターは、この設定を使用してこのような操作中にメモリリークに備えました。
移行に注意してください。 標準のLaravelテーブルに加えて、
-
stored_events
メタイベントデータ用の非構造化データのいくつかの列を持つメインESテーブル。イベントタイプを行に保存します。 重要な列aggregate_uuid
関連するすべてのイベントを受け取るために、集約のUUIDを保存します。 -
accounts
-ユーザーアカウントのプロジェクターのテーブルは、貸借対照表上の現在のデータをすばやく返すために必要です。 -
transaction_counts
完了したトランザクションの数をすばやく返すために必要な、ユーザートランザクションの数のプロジェクターのテーブル。
そして今、私は新しいアカウントを作成する要求で道を打つことを提案します。
アカウント作成
標準resource
ルーティングはAccountsControllerを記述します 。 store
メソッドに興味があります
public function store(Request $request) { $newUuid = Str::uuid(); // , uuid // AccountAggregateRoot::retrieve($newUuid) // ->createAccount($request->name, auth()->user()->id) // ->persist(); return back(); }
AccountAggregateRootは、ライブラリAggregateRootを継承します。 コントローラーが呼び出したメソッドを見てみましょう。
// uuid public static function retrieve(string $uuid): AggregateRoot { $aggregateRoot = (new static()); $aggregateRoot->aggregateUuid = $uuid; return $aggregateRoot->reconstituteFromEvents(); } public function createAccount(string $name, string $userId) { // // , recordThat, , , // .. ) $this->recordThat(new AccountCreated($name, $userId)); return $this; }
persist
メソッドは、 event-projector.php設定で指定さstoreMany
たモデル(このstoreMany
はstoreMany
メソッドstoreMany
呼び出します。
public static function storeMany(array $events, string $uuid = null): void { collect($events) ->map(function (ShouldBeStored $domainEvent) use ($uuid) { $storedEvent = static::createForEvent($domainEvent, $uuid); return [$domainEvent, $storedEvent]; }) ->eachSpread(function (ShouldBeStored $event, StoredEvent $storedEvent) { // , // QueuedProjector* Projectionist::handleWithSyncProjectors($storedEvent); if (method_exists($event, 'tags')) { $tags = $event->tags(); } // $storedEventJob = call_user_func( [config('event-projector.stored_event_job'), 'createForEvent'], $storedEvent, $tags ?? [] ); dispatch($storedEventJob->onQueue(config('event-projector.queue'))); }); }
プロジェクターAccountProjectorとTransactionCountProjectorはProjector
実装するため、記録と同期してイベントに応答します。
OK、アカウントが作成されます。 クライアントがそれをどのように読むかを検討することを提案します。
請求書の表示
// `accounts` id public function index() { $accounts = Account::where('user_id', Auth::user()->id)->get(); return view('accounts.index', compact('accounts')); }
アカウントのプロジェクターがQueuedProjectorインターフェースを実装している場合、イベントが順番に処理されるまでユーザーには何も表示されません。
最後に、口座からの資金の補充と引き出しがどのように機能するかを調べます。
補充と引き出し
もう一度、 AccountsControllerを見てください:
// uuid // // , public function update(Account $account, UpdateAccountRequest $request) { $aggregateRoot = AccountAggregateRoot::retrieve($account->uuid); $request->adding() ? $aggregateRoot->addMoney($request->amount) : $aggregateRoot->subtractMoney($request->amount); $aggregateRoot->persist(); return back(); }
アカウントを補充する場合:
public function addMoney(int $amount) { $this->recordThat(new MoneyAdded($amount)); return $this; } // "" recordThat // AggregateRoot*? // apply(ShouldBeStored $event), // 'apply' . EventClassName // , `MoneyAdded` protected function applyMoneyAdded(MoneyAdded $event) { $this->accountLimitHitInARow = 0; $this->balance += $event->amount; }
資金を引き出す場合:
public function subtractMoney(int $amount) { if (!$this->hasSufficientFundsToSubtractAmount($amount)) { // $this->recordThat(new AccountLimitHit()); // , // , // if ($this->needsMoreMoney()) { $this->recordThat(new MoreMoneyNeeded()); } $this->persist(); throw CouldNotSubtractMoney::notEnoughFunds($amount); } $this->recordThat(new MoneySubtracted($amount)); } protected function applyMoneySubtracted(MoneySubtracted $event) { $this->balance -= $event->amount; $this->accountLimitHitInARow = 0; }
おわりに
LaravelのCQRS / ESで「オンボーディング」のプロセスをできる限り水なしで説明しようとしました。 コンセプトは非常に興味深いですが、機能がないわけではありません。 実装する前に、次のことを覚えておいてください。
- 最終的な一貫性;
- DDDを別々のドメインで使用することが望ましいため、このパターンで完全に大規模なシステムを作成しないでください。
- イベントテーブルのスキーマの変更は非常に苦痛です。
- 責任を持って、イベントの粒度の選択にアプローチする価値があります。具体的なイベントが多いほど、テーブルに含まれるイベントが多くなり、イベントを処理するためにより多くのリソースが必要になります。
エラーに気付いてうれしいです。