LAMPでもイベントソーシングしたい

# 動機
旧来のLAMP的なシステムでイベントソーシングにリファクタリングしたいなぁと思いつつ、

1. イベントストアを新たに建てるのが大変
2. イベントストアの内容を参照系へ投影するプロジェクタも用意しないといけない
3. 結果整合性について考えないといけない
4. そこまで規模が大きいシステムではない

という理由でイベントソーシングは諦めていました。
そのため、どうせイベントソーシング出来ないんだからとドメインイベントについても
深く考えることをしてませんでした。

けれど以下の方法ならイベントソーシング出来るのではないか？と思い投稿してみます。

# 先に結論
RDBMSのトリガ機能を使えば良いのでは？


# 構成
LAMPですけど自分の好みで

* L = linux
* A = apache
* M = postgresql
* P = php

で考えます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/109302/3d1d5b44-1419-5e67-3433-d777292626ca.png)

php - postgresql 間を拡大してみると

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/109302/9cbaf73a-c2ba-c290-8687-389ddd1af19a.png)

[](
@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している感じです。

#構成案
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/109302/c1958eb6-d9d5-70a4-5651-36c57b7fac93.png)

[Postgresqlにあるトリガという機能](https://www.postgresql.jp/document/9.4/html/triggers.html)を使えばRDBMSのままイベントソーシングできるかなと思いました。
トリガ機能は他の主要なRDBMSにもあると思います。

例えばeventstoreとして以下のテーブルを用意して

```sql
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,
)
```

トリガ関数として以下のような関数を定義

```sql
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;
```

トリガを登録

```sql
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でもイベントソーシング出来そうだなと思い投稿しました。
今後周りの迷惑にならない程度に実践していこうかと思います。
この方法に対してとてつもないデメリットが存在するかもしれないのでその際はご指摘願います。