何が書いてあるか
- イミュータブルなデータモデルを設計しようとしたらつらかった
- とはいえ 合計 4 パターンのリソースとイベントを意識したら少し見通しが良くなった
はじめに
筆者は何を隠そうデータモデル全体を設計するのは初めてでした。。
友人と始めたプロジェクトで学習のためにと、データモデルの設計をするに至り、なぜかイミュータブルなデータモデルを試してみようという機運が高まり、結果的に痛い目をみました。ほんと積み上げが大事ですね
間違っている記述などあればコメントで教えていただけるととても嬉しいのでよろしくお願いします!
かかげた基本の設計方針
一旦以下のような設計方針でデータモデルの設計をはじめました。
- イミュータブルなモデルを設計する
- null は使用しない
- updated_at, deleted_at, is_active などを避けて システムカラムは created_at だけを目指す
- 複数人で編集する機能がないことから、1人で複数タブで動作させることは無視して「後勝ち」→楽観ロックのversionなどは使わない
- DB 全体での統一感を重視し、アプリ開発者が使いやすいことも考慮する
イミュータブルなにそれだったので調べ、「イミュータブル : 変わらない、不変な、不易の....」「イミュータブルなオブジェクトとは、作成後にその状態を変えることのないこと」というところから始まりました。なかなか暗雲垂れ込めてますね笑
やってみたこと
データモデルをつくる対象は、ブログ Web アプリです。
列挙する形になりますが、まずたたき台にするデータモデルを作成するまでにざっくりやったことを上げていきます
- 概念モデルの作成
- 基本方式の決定 → 上述。イミュータブルにいこうぜ!
- ナチュラルキーとサロゲートキーの方針決定
- 命名規則の言語化
- エンティティ(リソース, イベント)とリレーションの洗い出し
- 制約(外部キー制約, ユニーク制約など)の洗い出し
- 方針、機能一覧、概念モデルを実現できるように正規化しながら調整
おこったこと
予想通り、暗雲からめちゃくちゃ雨とか雷とかがふってきました。
Update されうるエンティティは、created_at で更新の有無を判定することになります。(汎用的な operation イベントテーブルを利用する手法などもあるかと思いますが)
例えばプロフィール付きのユーザーを表すのに、ユーザー, ユーザー詳細, 無効化したユーザーを登録するテーブル, ユーザーの画像, 無効化したユーザー画像を登録するテーブル... というふうにテーブルの数が膨大になりました。ここまではまだ想定内でしたが
- 無効化をどう表現するか
- ユニーク制約をどう表現するか
を考え始めたら、このデータもエルは Web アプリを実現するに足るのかこんがらがりました。
最終的にこんな感じで分けて考えました
こんがらがってきたので、一度分解して以下の ① ~ ④ の分類を行い、特徴的な部分ごとにデータモデルを考えていきました。
- イベント系 ①
- リソース系
- 無効化が不要 ②
- 無効化が必要
- ユニーク制約が不要 ③
- ユニーク制約が必要 ④
パターン別にイミュータブルなデータモデル設計
① イベント系 (ex. attendances )
リソース:勉強会(meetups)にリソース:ユーザー(users)がイベント:参加(attendances)することを表したテーブル群です。
イベント系はシンプルに 1 パターン考えて対応できそうでした。(もちろん、無効化を表現する切り出す方法はもっとあると思いますが。。。)
attendances | イベント系 |
---|---|
Create | attendances tbale に新しい行を挿入する |
Read | meetup_id と user_id から attendances テーブルの cretaed_at をみる。最新の created_at が現在の状態。ただし invalidate_attendances テーブルに同じ attendance_id が登録されていれば有効でないものとして扱う |
Update | cretae と同じ動作 |
Delete | attendances_id を指定して invalidate_attendances テーブルに挿入する |
② リソース系 無効化が不要 (ex. categories)
イベント:カテゴリ付け(categorizations)とリソース:カテゴリ(categories)を表現したテーブル群です。
categories には一度入力されたら無効化する機構がないため Update されるときはユニーク制約の有無に関わらず、created_at の最新をみています。
categories | リソース系 無効化が不要 |
---|---|
Create | 新しい行を挿入(ユニーク制約がある場合は確認する) |
Read | category_id をみる |
Update | Create と同じ |
Delete | なし |
③ リソース系 無効化が必要 ユニーク制約が不要 (ex. meetups)
イリソース:勉強会(meetups)を表現したテーブル群です。
リソースが無効化される可能性があるので、invalidate_meetups テーブルと join して Read, Update を行う必要があります。
一度無効化されたリソースが同じ meetup_id で復活することはない設計にしています。
meetups | リソース系 無効化が必要 ユニーク制約が不要 |
---|---|
Create | 新しい行を挿入 |
Read | meetup_id をみる。 ただし invalidate_meetups テーブルに同じ meetup_id が登録されていれば有効でないものとして扱う |
Update | Create と同じ |
Delete | invalidate_meetups テーブルに user_id を指定して新しい行を挿入 |
④ リソース系 無効化が必要 ユニーク制約が必要 (ex. emails)
リソース:メールアドレス(emails)を表現したテーブル群です。
想定した仕様は以下のようでした
- ユーザーはただ 1 つのメールアドレスをもつ
- ユーザーはメールアドレスを変更できる
- いま有効なユーザーのメールアドレスは一意になる
- ユーザーが無効化された場合はメールアドレスも無効化される
- あるメードアドレスが無効化されると、そのメールアドレスは他ユーザーが使用可能になる
これは今でもはっきりと整理しきれてないです。history テーブルで過去すべてのメールアドレスとユーザーの関連を保持する一方で、現在使用しているメールアドレスに関しては、emails テーブルの物理削除を駆使して一意に保つ方式です。
ユーザーが無効化された場合も、emails のメールアドレスを物理削除します。
emails | リソース系 無効化が必要 ユニーク制約が必要 |
---|---|
Create | 作成するメールアドレスが emails に存在しない場合には、新しい行を emails_histories と emails に挿入する(その他の外部キー制約は存在する) |
Read | 現在使用されている メールアドレスを取得する場合は emails を読む。ユーザーが使用していたメールアドレスを取得する場合は emails_histories を読む |
Update | Update する前メールアドレスを emails から物理削除し、create と同じ動作をする |
Delete | 該当行を emails から物理削除する |
最後に
と、進めてみましたがアプリ実装をする際にも慣れておらず、DBを扱うたびに考える時間が長くなってしまいそうで、今回はイミュータブルなデータモデルの設計は見送りました。
考える機会を得られたのはすごく良かったなと。