イミュータブルデータモデル
はじめに
CRUDのうちUPDATEがもっともシステムを複雑化する。更新には複雑なルールが伴うからだ。業務的に複雑なルールが存在するのは仕方ないこともあるが、システム、設計で複雑さを更に増さないようにしたい。UPDATEに着目し、その発生をできるだけ削ることによって複雑さをおさえるためには、まずデータモデルをそのように設計しておかなけれなならない。このイミュータブルデータモデルは、それを手助けする手法で、手順に沿って実施すればある程度のスキルのバラつきも吸収できるように組み立てられている。
手順
Step1. エンティティを抽出する
まずエンティティを抽出するところから始める。
5W1Hがエンティティの候補
Who 従業員,患者,プレイヤー,顧客,生徒,...
What 製品,サービス,コース,曲,...
When 時間,日付,月,年,年度,...
Where 送付先,URL,IPアドレス,...
Why 注文,返品,入金,出金,取引,…
How 請求書,契約書,注文書,…
例えば、
発注担当者が受注リストをもとに、商品の在庫を確認し、在庫があれば商品を注文者の注文時の送付先住所に発送する。
というように要求文書に対して、5W1Hに下線をひく。これがそれぞれエンティティとその属性の候補になる。
注: 実装は置いておこう
モデリングの段階で実装をプロパティファイルにするとかenumにするとか決め打ちで、エンティティとして表現しないのは止めよう。これはいわゆる早すぎる最適化のアンチパターンである。この段階でそのアイデアが最適な実装であるかどうか分かりっこない。 そしてシステム規模を正しく表すことができなくなることが、最大の問題である。システムの規模計測に使われるFPに、NESMA法と呼ばれるものがある。これは以下の式で表される。
code:NESMA
35 × ILF(システム内部管理のEntity)の数 + 15 × EIF(システム外部管理のEntity)の数
つまり、Entityの数がシステムの規模に比例することを示している。もしテーブルとして実装しないからといってエンティティとして表現しなければ、システムの規模が本来よりも小さく表現されてしまうことになる。
エンティティの名前
エンティティの名前は、短くその意味を的確に表現するものでなくてはならない。これはクラス名や変数名を付けるときと同じく共通のルールであるが、エンティティ名はその後の全て名前付け対象のものに影響してくるので、特に重要である。
以下のような、無くても意味が通じるワードは付けないようにしなければならない。
情報
データ
処理
〜物
マスタ
記録
管理
また時折、論理設計=日本語名称、物理設計=英語(ローマ字)名称を付けるとしてプロセス定義し、英語名称を付けるのを後工程にまわすプロジェクトに出くわすが、その必要はない。用語辞書を作って、和名/英名をセットで定義しておいて、同時に命名すればよいからだ。
ただし、このやり方では英名が長くなることがある。この場合、用語辞書には英名短縮名称も用意しておくと良いし、そういったことが現場でも行われている。この短縮名を付ける際に注意しておかなければならないことがある。短縮名を付けるルールは、日本では旧来の大手SIerの影響で、一律母音を抜くということが多く行われている。この「一律」が厄介で、全く発音できないものや視認性が非常に悪いものが作られるもとになる。
(和)企画 (英) KIKAKU → (短縮名) KKK
(和)キュー (英) QUEUE → (短縮名) Q
こういったものは、本番環境で緊急でSQLを実行しなきゃいけないときなどに、困らされることになる。発音できること、他としっかり区別できることを特に短縮名称を付ける際には重視しよう。
エンティティの3大要素
エンティティを考える際には、以下の3つの点について関係者各位で認識を揃えておく必要がある。
1. 単一性 (Oneness) 「何が1つのものであるか?」
2. 同一性 (Sameness) 「2つのものが同じであると、どういうときに言えるのか?」
3. カテゴリ (Category) 「それは何であるか? どんな分類で識別されるか?」
例えば、情報コンテンツサイトにおける記事エンティティは以下のような点を決める必要がある。
単一性 全6回ものの記事は、同じ1つの記事として扱う? それとも別々の6つの記事とする?
同一性 記事の内容はどんなに書き換えられても同じものとして扱う? どこまで書き換えられたら別の記事として扱う?
カテゴリ 記事の範囲はクライアント入稿記事だけを指す? それともライターに依頼した記事も含む? FAQみたいなものはそこに入る?
これはその組織や業務によって変わるものであり、絶対的な正解はない。
Step2. エンティティを分類する
洗い出したエンティティをリソースとイベントに分類する。基準は明確で属性に”日時”を持つかどうかである。
https://gyazo.com/37ff2e86d92f034dc298decfa6bdea6f
これはエンティティ名に「〜する」を付けてみることによって識別もできる。上記例では、「注文する」というのは自然に成り立つが、「会員する」というのは成り立たない。すなわちイベントは動詞から抽出したエンティティであることを意味する。
また、5W1Hの分類において、
Who, What, When, Where → リソース
Why, How → イベント
になる。
ここでの分類をER図に表すときには、エンティティ名に (R) や (E) を付けて、ひと目で識別できるようにしておくとよい。
注: 日時属性とは…?
「請求予定日」のように将来の予定を表すものや、「有効期限」「適用開始日」のようにデータのライフサイクルを表すものは、ここでいう"日時"属性ではない。業務のアクティビティを記録するのがイベントエンティティであり、その行われた時刻を記録するのがここでいう”日時”属性である。
注: マスタとトランザクションの分類は使わない
エンティティの分類には、日本の開発の現場では伝統的に「マスタ」と「トランザクション」というものが使われてきた。
マスタ - 変更されないもの
トランザクション - よく変更されるもの
という意味合いであるが、これは判別・使用用途ともに曖昧で、自転車置き場の議論を呼び時間を無駄にすることがある。会話の中で使うのはよいが、設計ドキュメント、コードには一切使わないようにした方が良い。
code:不毛な議論
「システムで更新しないものを『マスタ』とします」
「今度追加されたマスタメンテ機能で更新されるんじゃない…?」
「マスタメンテ機能はアドミンしか使わないからノーカンです」
「はい。…あ、でも社員マスタは社員管理システムから送られてきて同期させるのですが、これは…?」
Step3. イベントエンティティには1つの日時属性しかもたないようにする
Step2.で分類したリソースとイベントによって設計のやり方が分かれる。まずはイベントについてである。
イベントのエンティティにはただ1つの日時属性しか持たないようにすることが、ここでまずやることである。
https://gyazo.com/41c5d00a405480563b504e1fcb87a410
よく考えることなしに作られたイベントエンティティには上図左のように、多くの日時属性を含むことがある、これを分解しそれぞれイベントエンティティを作るのである。
左のように複数の日時属性を含んでいると、すなわちこのエンティティに対する更新が発生すること意味する。しかも「注文日時を更新するときは、業務上『注文』のときで注文会員ID、登録日時、更新日時以外は値が入ってはならない」とか「注文確認日を更新するときは、業務上『注文確認』のときで、注文確認者、注文確認日、更新日時以外はUPDATEしてはならない」とか、更新ルールが様々生まれてしまうことになる。おまけに、このルールはエンティティからは読み取れず、個別の業務の画面やバッチの設計書に書かれることになるだろう。とかく、複雑さが一気に増すのである。
右のようにただ1つの日時属性しか持たないように設計されていれば、イベントエンティティの性質上、各エンティティには1回のINSERTのみ行われ、一切UPDATEされないようになる。したがって複雑な更新ルールは発生しないし、業務のアクティビティがエンティティとして明確に表されるようになる。そして何より、正規化の原則であるところの「One fact in one place」が自然と実現できていることになる。
発展: イベントが1つの日時では捉えきれない場合
業務上認識するイベントが、始まりと終わりがあってそれぞれ日時が異なるケースが多々ある。このときも基本的な原則は「イベントエンティティには1つの日時属性しかもたないようにする」である。
といっても、元の大きな意味でのイベントがどういう状態かを見たいケースがあるので、これを表すエンティティも欲しい。ということで、次に示すロングタームイベントパターンが適用できる。
https://gyazo.com/7adcded899d74fe1a11a29168ff43f3b
「入会」という長い期間でのイベントに対し、その中で発生する詳細イベント(入会申し込み、審査、会員証発行)はそれぞれエンティティとして表す。詳細イベントは共通して日時属性を持つので、スーパータイプとして入会アクティビティを持つ。
こうすると、「入会申し込み」の業務では、入会、入会アクティビティ、入会申し込みにそれぞれデータがINSERTされ入会エンティティの現在入会ステータスは「入会申し込み」になる。続く「審査」の業務では、入会アクティビティ、審査にデータがINSERTされ、入会の現在入会ステータスが「審査」に更新される。こうして詳細イベントはただ1つの日時属性をもつエンティティでINSERT ONLYなデータライフサイクルが実現され、更新が発生するのはロングタームイベントの「入会」のみになる。
注: イベントエンティティの重要性
リソースとイベントを比べたときに、業務上重要、すなわち金を産むのはイベントである。イベントエンティティには事業活動そのものが記録されることになるからである。
したがって、データモデル(ER図)を書くにあたっても、イベントエンティティから洗い出し、これを図の中心に据えるようにしよう。
https://gyazo.com/a70e57825773ac153db4e428e0eef7ea
Step4. リソースに隠されたイベントを抽出する
Step3.はイベントエンティティについての設計だったので、今度はリソースエンティティについてである。リソースエンティティに更新日時を持たせたいかどうかをそれぞれ吟味してみよう。そうしたときに、何かリソースエンティティにも更新日時を持たせておきたいと思ったら、それにまつわるイベントエンティティが洗い出せていない証拠である。
https://gyazo.com/bc0313d42a89f3fccee2fba333fa4251
上記のようなケースにおいて、会員の更新日時を持たせたいとしたら、まず更新しなきゃいけないケースを洗い出そう。
会員が自分自身で会員情報変更ページから変更する
規約に違反した会員であったため、お客さまセンターのオペレータが強制退会をおこなう。
会員からの「誤った退会をしてしまったので取り消して欲しい」との問合せを受けて、お客さまセンターのオペレータが会員の復会をおこなう。
例えば上記のように3つのケースがあることが分かったとしたら、これらをイベントエンティティとして抽出する。
https://gyazo.com/14f07cd29c31137a70174e1a12169766
そうすると、それぞれのイベントで記録したい属性も異なることに気づくだろう。このリソースに隠れたイベントの抽出をせずに、リソースエンティティに更新日時を付けただけでお茶を濁していると、後から付け足し付け足しでイベントが乱雑に増えていくもとになる。
リソースに「更新日時」を持たせたい気持ちは、その隠れたイベントの存在の試験薬であり、一度立ち止まってそれらを抽出するようにしよう。
すべてのエンティティに付ける登録日時や更新日時
よく全てのエンティティに一律で登録日時や更新日時を付ける設計ルールを見かける。さらには、それを自動的に設定してくれるフレームワークまで存在する始末である。
だが、それが何のために存在しているかをよく考えてみよう。
そういうルールのプロジェクトの人たちに聞くと、「何か問題が合ったときにトレースするため」という答えが返ってくることが多いが、リソースに「更新日時」(や更新者ID、更新プログラム)だけあっても、1世代の更新がいつ行われたかしか分からない。これで問題の原因究明に役立てることはまずなく、リカバリもできない。本当にそういう課題に対処したいのであれば、変更履歴を別のエンティティとして用意し、変更前の状態をもっておく設計をしなければならない。
また一方、イベントエンティティについても、Step.3により登録/変更の日時をただ1つだけしかもたないように設計されるので、同じ意味を持つ一律の登録日時や更新日時は不要だ。
したがって、そもそも登録日時や更新日時という属性がリソースにもイベントにも不要であることになり、元々の一律付与のルールが思考停止の産物だったことが分かる。
https://gyazo.com/e0140755706c16b077ce4dd8b1751991
プロジェクトのデータモデルが思考停止してないかどうかは、どのシステムにもたいてい存在する「都道府県」を見ると良い。廃藩置県を扱うシステムじゃなければ一律日時カラムは要らないよね😜
リソースの区分
形式上はロングタームイベントのステータスと似ているが、リソースエンティティには区分という性質があり業務上扱いが変わることがある。
例えば社員のうち、退職社員のデータも業務上残しておく必要があるが、個人情報保護の観点から氏名は消さなきゃいけない、という場合である。
https://gyazo.com/05f8386d3edf6ca898dfc534d6ac6ab0
そういう場合は、社員に属性「社員区分」を持って、その区分ごとにサブタイプとして「現役社員」「退職社員」をエンティティとして表現する。
業務上扱いが変わるものはすべて、このように区分をもとにサブタイプとして表現するようにしよう。これは以前述べたシステム規模を正しく表現するのに役立つ。モデルに表現されていなければ、個々の画面/バッチ設計書を読み解かなければいけなくなるからだ。
このスーパータイプ/サブタイプに関して実装をどうするか気になる人がいるかもしれない。これはパターンとして3つ存在する。
1. シングルテーブル継承
ほとんどの業務が区分横断で検索し、区分によるデータ属性の差異も少ない場合、以下のように1つのテーブルにまとめてしまう。
https://gyazo.com/1f4914204692ac03d2fb344a2db66974
2. 具象テーブル継承
横断で検索することがほとんどなく、サブタイプ毎の属性の差が大きい場合は、サブタイプごとに1つずつスーパータイプの属性を含めた形でテーブルを作る。
https://gyazo.com/bcf36bda30b0d573d5171ef94b4d896d
3. クラステーブル継承
横断で検索するケースが割と存在し、サブタイプ毎の属性の差も大きい場合は、そのままそれぞれテーブルを作る。
リソースの履歴と世代
リソースに関する変更のイベントは、履歴の性質をもつことが多い。これも単に履歴テーブル作ってそこに入れておけばいい、といった物理設計のレイヤのパターンを使って思考停止してしまうと後々問題になることがある。
そもそも過去行ったリソースに対するアクションを記録するだけであれば、前述のとおりイベントエンティティとして普通に設計すればよい。雑に変更履歴とせずに、そこにはどういう業務がありうるかを洗い出すことが重要である。
少し難しいのは未来に対する変更である。これは「履歴」というより「世代」である。これを扱うにはいくつかのパターンがある。
単純なものは適用開始日、終了日を属性に持たせる方式であるが、これは扱うすべてのSQLの条件に適用開始日、終了日のBETWEENを加えなきゃいけないので、非常に開発負荷が高まる。したがってあまり採用することはない。
https://gyazo.com/c86177b456e451cb8c74ede4394075ea
ただし、これも過去と未来を分け、過去分だけをシングル世代テーブルパターンで実装するのはキレイに設計できる。
https://gyazo.com/3f661ff9c6f787017d42e0fcc98c49c8
シングル世代テーブルパターンの開発負荷を下げるために、最新世代しか必要ないシステム向けにはMaterialized Viewなどを使って反映させていく、有効世代ビューパターンがとりうる。
https://gyazo.com/70993ca34c80d96a49f88f30738d2db9
そうでなく、過去も未来もひっくるめて世代を同一システムで扱わなきゃいけなければ、次に示す世代バージョンタグ付けパターンを使う。これはGitのバージョンとタグの関係性に似ている。
https://gyazo.com/021c9d9d651a53908eb80b2432aa0984
リソースの削除
「削除フラグ」や「削除日時」は悪である。といった設計思想が広まるのは良いことだが、これもまた物理設計のパターンで思考停止してはいけない。「削除フラグ」を持たせたくなるのはリソースエンティティに関してであるが、まずはこれも更新日時と同様にそれにまつわるイベントを洗い出すことが重要だ。
それに加えて、削除したいリソースがイベントと関連付いているので消せないということがある。
https://gyazo.com/05f8386d3edf6ca898dfc534d6ac6ab0
それでも「退職社員」の例で述べたように、残してはならない機微なデータは消せるように設計されて無くてはならない。そして初めて、「社員の削除」すなわち「退職社員」は社員区分の違いで表現するということが議論できるようになる。
Step5. 非依存リレーションシップを交差エンティティにする
最後のステップはリソースエンティティ間、イベントエンティティ間のリレーションシップについてである。これらのリレーションシップが非依存である場合、一方のエンティティのキーを、もう一方に外部キーとして持たせてしまうと、強い依存関係が生まれる。これは、業務の移り変わりによっては、登録する順番が変わってしまい、「外部キーをNULLで登録し、関連がついたら更新する」なんて事態をうむ。
よくある例が部門-社員のケースだ。
https://gyazo.com/b37b03ebcc9d5ddfd72660bbc1ae04c9
部門と社員の関係性が1対多であるからといって、社員に部門IDを属性として持たせてしまうと、表現できないデータが存在することがある。
部門配属前の新入社員
どの部門にも属さない役員
このようなデータのために部門IDをnullableにしておいて、配属時に更新するということをやってしまうことになる(またはシステム上部門IDにnullが入らないので、無理やり新入社員を人事部配属扱いにしたり、「役員」という部門を作ったりしているものもある)。
このようにリソース間のリレーションシップを考えるにあたっては、「1対多」や「多対多」などのカージナリティだけに着目するのではなく、両者の依存関係を考えるようにしよう。上記例だと通常の企業であれば、「部門」は「社員」がいなくても存在できるし、「社員」は「部門」に配属されずとも存在できるだろう。そういう非依存のリレーションシップの場合は、間に交差エンティティを置く。
https://gyazo.com/0425a8ae802f1eb46348c74942dd25a1
こうすることで、部門、社員は独立してレコードを作ることができ、所属が決まれば所属のエンティティにその事実をINSERTするだけでよい。
イベント同士のリレーションシップに関しても同様だ。
例えば、「複数の注文をまとめて請求する」ということを実現するのに、注文からみて請求が「多対1」の関係性だからといって、請求IDを注文エンティティにもつと、注文発生時点では請求IDをnullにしておかざるをえなくなる。
https://gyazo.com/88b331ea6e59345202b5a2b00f562929
こういう時系列の逆転したリレーションシップは作らず、間に交差エンティティを置くことで、リソースのときと同様に、注文時点、請求時点それぞれの業務でUPDATEを発生させることななくなる。
https://gyazo.com/3dfcfac60f97b945340a50c9536a4c70
交差エンティティの履歴
リソース間の交差エンティティは、イベントのように見えることもある。例えば部門と社員の間の交差エンティティは配属日時を属性に持てば「配属」とみなせる、というように。
https://gyazo.com/e15636d3b148fc5157d8955f683009f2
そういう「配属」を記録したい場合でも、これは「所属」というリソース間の交差エンティティとは別に作る方がよい。現在の部門と社員の関係性を調べるという頻出のユースケースに置いて、その結びつきがイベントしかない場合は、現在「最新の状態」を探しにいく必要が出てくるからだ。
This work is licensed under a Creative Commons Attribution 3.0 Unported License.