動機
旧来のLAMP的なシステムでイベントソーシングにリファクタリングしたいなぁと思いつつ、
- イベントストアを新たに建てるのが大変
- イベントストアの内容を参照系へ投影するプロジェクタも用意しないといけない
- 結果整合性について考えないといけない
- そこまで規模が大きいシステムではない
という理由でイベントソーシングは諦めていました。
そのため、どうせイベントソーシング出来ないんだからとドメインイベントについても
深く考えることをしてませんでした。
けれど以下の方法ならイベントソーシング出来るのではないか?と思い投稿してみます。
先に結論
RDBMSのトリガ機能を使えば良いのでは?
構成
LAMPですけど自分の好みで
- L = linux
- A = apache
- M = postgresql
- P = php
php - postgresql 間を拡大してみると
[](
@startuml
left to right direction
node php
database postgresql {
node tableA
node tableB
node tableC
}
php --> tableA : CRUD
php --> tableB : CRUD
php --> tableC : CRUD
@enduml
)
こんな感じで個々の処理でそれぞれテーブルにCRUDしている感じです。
構成案
Postgresqlにあるトリガという機能を使えばRDBMSのままイベントソーシングできるかなと思いました。
トリガ機能は他の主要なRDBMSにもあると思います。
例えばeventstoreとして以下のテーブルを用意して
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
CREATE TABLE public.events
(
event_id uuid NOT NULL DEFAULT uuid_generate_v4(),
event_name text COLLATE pg_catalog."default" NOT NULL
version integer NOT NULL,
data json NOT NULL DEFAULT '{}'::json,
)
トリガ関数として以下のような関数を定義
CREATE OR REPLACE PROCEDURE book_added(version int, data json) AS $book_added$
BEGIN
-- イベントに付随するjsonを用いてinsertなりupdateなりdeleteなりを行う.
END;
$book_added$
LANGUAGE plpgsql;
CREATE OR REPLACE PROCEDURE book_borrowed(version int, data json) AS $book_borrowed$
BEGIN
-- イベントに付随するjsonを用いてinsertなりupdateなりdeleteなりを行う.
END;
$book_borrowed$
LANGUAGE plpgsql;
CREATE OR REPLACE FUNCTION event_exec() RETURNS trigger AS $event_exec$
BEGIN
CASE NEW.event_name -- イベント名で振り分け
WHEN 'book.added' THEN -- 例えば"本を追加した"
CALL public.book_added(NEW.version, NEW.data);
WHEN 'book.borrowed' THEN -- 例えば"本を借りた"
CALL public.book_borrowed(NEW.version, NEW.data);
ELSE
RAISE EXCEPTION 'not found : %', NEW.event_name;
END CASE;
RETURN NEW;
END;
$event_exec$
LANGUAGE plpgsql;
トリガを登録
CREATE TRIGGER event_exec
AFTER INSERT
ON public.events
FOR EACH ROW
EXECUTE PROCEDURE public.event_exec(\x);
後はeventstoreにInsertだけをするようにすればイベント内容に沿って各テーブルを更新してくれます。
これのメリットとしては
- 参照系の処理は触る必要がない
- 更新系の処理を一度に全てを変える必要がない
- 結果整合性のタイムラグが限りなく低い
- 業務知識をドメインイベントで考えることが出来る
が考えられます。
逆にデメリットとしては
- イベントストアの内容を参照系へ投影するプロジェクタ部分がPL/pgSQLになる
かなと。
参照系の処理は触る必要がない
元々定義してあるテーブルに、そのまま最新の状態が保持されるので参照系はそのままで良いです。
全ての更新系がイベントソーシングに代わった時に、テーブルの最適化やスケーリングについて検討できます。
更新系の処理を一度に全てを変える必要がない
元々更新系の処理は直接テーブルに対してCUDしています。
業務知識の整理がついた箇所から
-イベントを挿入-> eventstore -CUD-> 各テーブル
に変更していけます。
最終的に全ての更新系がイベントソーシングになれば、イベント保存先をイベントストアに変更するとか検討できます。
結果整合性のタイムラグが限りなく低い
いわゆるイベントソーシングの構成だと
-イベントを挿入-> イベントストア -イベントを通知-> 参照系RDBMS -イベントを検知。テーブルに反映-> 各テーブル
となりある程度のタイムラグが発生します。
けれどトリガで定義しておけば同一資源上にいますし、トランザクションを貼れば全てのトリガが反映されるまでブロックしてくれます。
イベントソーシングを考える上で面倒くさい結果整合性についてある程度無視できます。
業務知識をドメインイベントで考えることが出来る
別にイベントソーシングにしなくても業務知識をドメインイベントで考えることが出来るとは思いますが
イベントに分けたところでそのエンティティの操作履歴が残るわけでもなく
結局各テーブルを直接更新できる環境にいるので
Aした --> テーブル更新
Bした --> テーブル更新
Cした --> テーブル更新
とするよりも
AしたBしたCした内容をまとめて --> テーブル更新
で良いじゃんと思ってしまい(自分は)なかなかドメインイベントの検討までいけません。
-イベントを挿入-> eventstore -CUD-> 各テーブル
の流れを作ることで
- ユースケースAならイベントA, B, Cが考えられる
- イベントAが発生するとxxが変更される
と分けて考えることが出来るかなと。
イベントストアの内容を参照系へ投影するプロジェクタ部分がPL/pgSQLになる
postgresだと
- PL/pgSQL
- PL/Tcl
- PL/Perl
- PL/Python
から選べますがどちらにしても
- イベントAが発生するとxxが変更される
の部分の実装が分かれてしまいます。
まあこの部分には業務知識を入れず純粋に振り分けと変換処理に徹すれば良いかと。
まとめ
机上の空論で申し訳ないのですがトリガを使えばRDBMSでもイベントソーシング出来そうだなと思い投稿しました。
今後周りの迷惑にならない程度に実践していこうかと思います。
この方法に対してとてつもないデメリットが存在するかもしれないのでその際はご指摘願います。