🚯

イミュータブルデータモデルにおける "削除フラグ" の表現

2023/09/04に公開

イミュータブルデータモデリング

Q. イミュータブルデータモデリングについて教えてください。

A. 以下、ChatGPT の回答です。

イミュータブルデータモデリング(Immutable Data Modeling)は、データモデリングのアプローチの一つで、データを不変(変更不可)な形式で扱う方法です。このアプローチでは、データが一度生成されたら、その値を変更せず、新しいバージョンを作成することが一般的です。イミュータブルデータモデリングは、分散システム、バージョン管理、データの整合性、履歴のトラッキングなどのさまざまな領域で有用です。

以下は、イミュータブルデータモデリングの主要な特徴と利点です。

  1. データの安定性と信頼性: データが不変であるため、一度データが記録されたら、それを変更することができないため、データの信頼性と安定性が向上します。これは、エラーやバグがデータを破損させる可能性を減少させます。

  2. 履歴のトラッキング: データが変更される代わりに新しいバージョンが作成されるため、過去のデータの履歴を容易にトラッキングできます。これは、バージョン管理やデータの変更履歴の確認に役立ちます。

  3. 分散システムでの利用: イミュータブルデータモデリングは、分散システムで特に有用です。データが不変であるため、異なるノード間でのデータ同期やデータの競合を管理しやすくなります。

  4. 並行処理のサポート: 複数のプロセスやスレッドが同時にデータにアクセスする場合、不変なデータモデルを使用すると、データ競合を回避しやすくなります。

  5. モデルの単純化: データが不変であるため、データモデルが単純化され、データの状態変更を追跡するための複雑なロジックが不要になります。

  6. キャッシュと効率性: イミュータブルデータモデルは、キャッシュ効率を向上させることができます。変更のないデータはキャッシュに格納しやすく、高速な読み取りアクセスを提供します。

イミュータブルデータモデリングは、関連するデータ構造やアルゴリズムと組み合わせて使用され、特定のアプリケーションや要件に合わせて調整できます。ただし、すべてのデータが不変である必要はなく、一部のデータが可変である場合もあります。データベースやプログラム内の特定のデータ領域においてイミュータブルデータモデリングを適用することが一般的です。

メリットを見る限り、データモデリングにおいて有効な選択肢であると言えるでしょう。

WEB+DB PRESS Vol.130 の「特集1 イミュータブルデータモデルで始める 実践データモデリング」では、「更新」が複雑さの源泉 であると言及されています。

「変更できる」ということは、様々な業務イベントが存在している可能性あること を意味します。

「変更」を意識せずにモデリングすると、あるオブジェクトに対して様々な業務イベントが混在することになり、これが 複雑さ につながります。

ユーザー管理における削除を例に考えてみる

ユーザー管理の要件を簡単に定義します。

  • ユーザー情報として以下を管理できる
    • 名前
    • Eメールアドレス
    • 作成した日時
    • 作成したユーザー
    • 更新した日時
    • 更新したユーザー
  • ユーザーは削除できる(削除後はユーザー一覧に表示されない)
    • 削除された日時や削除したユーザーも知りたい
  • ユーザー情報の更新やユーザーの削除を履歴として追いたい

削除フラグで考えてみる

まずは、単一のテーブルでユーザーの削除を削除フラグ is_deleted で表現してみます。

DBML
Table users {
  id integer [primary key]
  name varchar
  email varchar
  created_at timestamp
  created_by integer
  updated_at timestamp
  updated_by integer
  is_deleted boolean
}

Ref: users.created_by > users.id

Ref: users.updated_by > users.id

ユーザー情報として管理したい情報は記録できるでしょう。

削除も更新として捉えれば、削除時に is_deleted を True に変更するだけでなく、updated_at updated_by も合わせて更新することで管理できそうです。

しかし、「ユーザー情報の更新やユーザーの削除を履歴として追いたい」という要件を満たせていません。

以下のようなケースの場合、誰が何をどのように変更したかまでは履歴として追うことができません。

  1. 山田 太郎さん(id: 0)が自分自身を新規作成した
  2. 山田 太郎さんが佐藤 次郎さん(id: 1)を新規作成した
  3. 佐藤 次郎さんが自分のEメールアドレスを「jiro1234@example.com」に変更した
  4. 山田 太郎さんが佐藤 次郎さんの名前を「田中 次郎」に変更した
  5. 山田 太郎さんが田中 次郎さんを削除した

4.の時点の users テーブルはこうなっていることが予想されます。

users

id name email created_at created_by updated_at updated_by is_deleted
0 山田 太郎 taro.yamada@example.com 1693528215000 NULL 1693528215000 NULL False
1 田中 次郎 jiro1234@example.com 1693614615000 0 1693701015000 0 True

ユーザーが作成されたタイミングでの名前やEメールアドレスがなんだったのかは不明です。

そのため、ユーザーID 1 の田中さんが佐藤さんだったこともデータ上では追えないということになります。

分かることは、ユーザーID 1 の田中さんが山田さんによって、2023-09-03 09:30:15 に削除されているということです。

ユーザー詳細を分離 + 削除日時

履歴を追えるように、ユーザー詳細をイミュータブルに設計してみます。

ユーザー情報の更新イベントとして、イベントが発生する度に、テーブルにレコードをインサートすることでデータを管理できます。

また、ユーザーの削除を 誰がいつ削除したのか も管理できるように削除日時で表現します。

この部分は UPDATE をおこなう必要があります。

DBML
Table users {
  id integer [primary key]
  deleted_at timestamp
  deleted_by integer
}

Table user_detail_histories {
  id integer [primary key]
  user_id integer
  name varchar
  email varchar
  created_at timestamp
  created_by integer
}

Ref: users.deleted_by > users.id

Ref: user_detail_histories.created_by > users.id

Ref: user_detail_histories.user_id > users.id

先述したケースで、履歴としてデータを追うことができるでしょうか。

users

id deleted_at deleted_by
0 NULL NULL
1 1693701015000 1

user_detail_histories

id user_id name email created_at created_by
0 0 山田 太郎 taro.yamada@example.com 1693528215000 0
1 1 佐藤 次郎 jiro.sato@example.com 1693614615000 0
2 1 佐藤 次郎 jiro1234@example.com 1693693815000 1
3 1 田中 次郎 jiro1234@example.com 1693697415000 0

ユーザーID 1 の田中さんが佐藤さんだったことも、Eメールアドレスの変更を自分自身でおこなっていることも読み取れます。

そして、佐藤さんが、山田さんによって 2023-09-03 09:30:15 に削除されているということも分かります。

これなら、要件を満たせそうです。

削除もイベントとして扱うように設計する

最後に、ユーザー情報の更新や削除をイベントとして扱うように設計してみます。

  • ユーザー情報の更新イベント
  • ユーザーの状態更新(アクティブ状態→削除状態)イベント

イベントが発生する度に、テーブルにレコードをインサートすることでデータを管理できます。

つまり、UPDATE はおこないません。

DBML
Table users {
  id integer [primary key]
}

Table user_detail_histories {
  id integer [primary key]
  user_id integer
  name varchar
  email varchar
  created_at timestamp
  created_by integer
}

Table user_status_histories {
  id integer [primary key]
  user_id integer
  status varchar
  created_at timestamp
  created_by integer
}

Ref: user_detail_histories.created_by > users.id

Ref: user_status_histories.created_by > users.id

Ref: user_detail_histories.user_id > users.id

Ref: user_status_histories.user_id > users.id

先述したケースで、履歴としてデータを追うことができるでしょうか。

users

id
0
1

user_detail_histories

id user_id name email created_at created_by
0 0 山田 太郎 taro.yamada@example.com 1693528215000 0
1 1 佐藤 次郎 jiro.sato@example.com 1693614615000 0
2 1 佐藤 次郎 jiro1234@example.com 1693693815000 1
3 1 田中 次郎 jiro1234@example.com 1693697415000 0

user_status_histories

id user_id status created_at created_by
0 0 active 1693528215000 0
1 1 active 1693614615000 0
2 1 deleted 1693701015000 1

ユーザーID 1 の田中さんが佐藤さんだったことも、Eメールアドレスの変更を自分自身でおこなっていることも読み取れます。

もちろん、佐藤さんが、山田さんによって 2023-09-03 09:30:15 に削除されているということも分かります。

この設計でも、要件を満たすことができそうです。

イミュータブルな設計におけるメリット・デメリット

メリット

変更に強く、拡張性がある

ユーザー管理の仕様変更があり、ユーザー一覧から消えてしまう削除だけではなく、ログインができない状態である「アーカイブ」もできるように修正するとしましょう。

  • アクティブ:ユーザー一覧表示あり・ログイン可能
  • アーカイブ:アーカイブ一覧表示あり・ログイン不可
  • 削除:一覧表示なし・ログイン不可

そうなった場合、users テーブルはどうなるでしょうか。

archived_atarchived_by を追加して管理する必要が出てくるでしょう。

DBML
Table users {
  id integer [primary key]
  deleted_at timestamp
  deleted_by integer
  archived_at timestamp
  archived_by timestamp
}

Table user_detail_histories {
  id integer [primary key]
  user_id integer
  name varchar
  email varchar
  created_at timestamp
  created_by integer
}

Ref: users.deleted_by > users.id

Ref: users.archived_by > users.id

Ref: user_detail_histories.created_by > users.id

Ref: user_detail_histories.user_id > users.id

もし今後もこのようにユーザーのステータス追加が行われる場合、その度にカラム追加を伴う修正が必要になってきます。

では、削除を状態変更イベントとして扱うように設計し、ステータスとしてイミュータブルに設計した場合はどうでしょうか。

status の取り得る値に archived が増えますが、カラム追加は不要です。

もし仮に、別のステータスが新たに増えても問題ないでしょう。

また、アーカイブしたユーザーを再度アクティブに戻せる仕様だった場合、archived_atarchived_by を NULL に戻すことなります。

そうなると、過去にアーカイブだったという事実を追えなくなります

user_status_histories の場合、アーカイブしたイベントと再度アクティブに変更したイベントのレコードを追加できるので、履歴として追うことができ、過去にアーカイブされたことが分かるでしょう。

user_status_histories

id user_id status created_at created_by
0 0 active 1693528215000 0
1 1 active 1693614615000 0
2 1 archived 1693701015000 1
3 1 active 1693708215000 1
4 1 deleted 1693711815000 1

ユーザーID 1 の田中さんが active -> archived -> active -> deleted というステータスの変更履歴があることが分かります。

このように考えていくと、削除を状態変更イベント として扱うことで 変更に強く、拡張性がある 設計にできると思います。

未来の状態も表現できる

変更に強く、拡張性がある という部分に近いですが、未来の状態を表現しやすいとも言えます。

退職日が決まったユーザーを、指定した日時になったタイミングで削除できるようにするという仕様変更があったとしましょう。

この仕様変更には、valid_start_at というステータス変更を適用開始する日時を表すカラムを追加するだけで対応できます。

user_status_histories

id user_id status created_at created_by valid_start_at
0 0 active 1693528215000 0 1693528215000
1 1 active 1693614615000 0 1693614615000
2 1 archived 1693701015000 1 1693701015000
3 1 active 1693708215000 1 1693708215000
4 1 deleted 1693711815000 1 1696086000000

ユーザーID 1 の田中さんが 2023-10-01 00:00:00 に削除されるように、2023-09-03 12:30:15 にステータスを変更されていることが読み取れます。

履歴としても管理でき、分かりやすくできているかと思います。

デメリット

レコードは必ず INSERT する

ユーザー削除を REST API のエンドポイントに落とし込むと、DELETE /users/{USER_ID} となるでしょう。

バックエンドの処理としては「削除」という定義でも、SQL では INSERT になります。

ユーザー詳細履歴の更新も REST API のエンドポイントにすると PUT や PATCH ですが、SQL では INSERT になるでしょう。

DELETE や UPDATE ではないので、バックエンドの処理を実装では HTTP メソッドと分離して考える必要があります。

イミュータブルな設計に挑戦した際、このあたりは最初戸惑ってしまう人が多いように感じるので、注意が必要かなと思います。

データ量が増える

先述したように、イミュータブルデータモデルをデータベース設計に反映させるとレコードを INSERT し続けることになります。

つまり、データ量は増えていくことが予想されます。

変更内容がない場合は INSERT せずに成功応答を返すなど、データ量を抑える方法を検討した方が良さそうです。

また、不要なデータを削除できる仕様であれば定期的に削除するなどで対策できるかもしれません。

まとめ

変更履歴のトラッキングが必要ならイミュータブル

ユーザー削除という業務イベントに焦点を当てて、データモデリングとテーブル設計をしてみました。

当時の情報を表示したいという要件から、世代・履歴管理が必要なケースは多いと思います。

そんな時はイミュータブルな設計が必要になってきそうですね🤔

削除フラグや削除日時は危険

「安易な削除フラグや削除日時は危険だ。悪だ。」 ということを聞いたことがある人も多いでしょう。

今回のケースでも、削除フラグでは要件を満たすことは難しそうでした。

やはり、安易にフラグという思考は仕様変更に弱いなぁ...と改めて思いました。

参考書籍

  • WEB+DB PRESS Vol.130
    • 特集1 イミュータブルデータモデルで始める 実践データモデリング
コラボスタイル Developers

Discussion