前置き
いままでいろんなデータモデリングの本に触れてきましたが、いまいち論理データモデルから物理データモデルへの変換の際のプロセスに違和感をずっと感じていたため、現代風にわたしなりにフレームワークを書き起こしてみました。
「変更に強いデータアーキテクチャのための段階的モデリング・フレームワーク」**として、このフレームワークは、関心事を
「ビジネスの真実」→ 概念データモデル
「論理的な理想形」→ 論理データモデル
「物理的な現実解」→ 物理データモデル
の3フェーズに明確に分離し、各フェーズで適用すべき設計原則を定義します。
フェーズ1:概念データモデリング (Conceptual Data Modeling)
このフェーズでは、ビジネス上で登場するエンティティおよびそれを構成する属性のみ を出します。
なので、エンティティ間の多対多を解消するための
中間テーブルなどは、意図的に出さないことを推奨します。
(ただし、ビジネスエンティティが、多対多を解消する中間の役割を偶然担うような場合には、必ず出しておきます。)
目的 (Objective)
・システムが扱う 「ビジネスの関心事」と「ビジネス用語」 を特定し、関係者間で共通の認識(ユビキタス言語)を確立すること。
・ビジネス上でどのようなエンティティ、つまり「何(What)」を扱うのかを定義し、技術的な実装詳細を完全に排除します。
前提条件 (Preconditions)
ビジネス要件、ドメインの概要、主要なステークホルダーが(大まかに)特定されている。
行うこと (Activities)
-
主要なビジネスエンティティ(例:
顧客,注文,商品)を網羅的に洗い出す。 -
エンティティが持つ、ビジネス上重要な属性(例:
顧客名,注文日)を特定する。 -
エンティティ間のビジネス上の関係性(例:「顧客が注文を出す」「注文が商品を含む」)を定義する。
M:N(多対多)の関係性は、ビジネスエンティティとして意味がない限り(例:予約)、ここでは解決しない(例:注文と商品のM:N関係はそのままにする)。
事後条件 (Postconditions)
ステークホルダー全員が合意した、ビジネス用語と主要エンティティの関係性を示す概念ER図(あるいはドメインモデル図)。
用いる適用原則 (Applied Principles)
ドメイン駆動設計 (DDD)
エンティティ、値オブジェクト、ユビキタス言語の特定。
事例 (Examples)
・顧客 -- (が) -- 注文する
・注文 -- (に) -- 商品 (が) -- 含まれる (M:N)
注意点 (Cautions)
絶対に
データ型、主キー/外部キー、DBの種別(RDB/NoSQL)、正規化、中間テーブルといった 「技術的・論理的実装」 をこのフェーズで議論してはいけない。
ここでは「データベース設計」ではなく、「ビジネスのモデリング」である。
フェーズ2:論理データモデリング (Logical Data Modeling)
この工程をすっ飛ばして、物理データモデルに行っている人をたまに見かけますが、絶対にやめてください。
間違った論理データモデルは、当然間違った物理データモデルを生み出し、
それをもとに考えられた物理データアーキテクチャとして構築してしまうと、
手戻りコストが計り知れません。
目的 (Objective)
・概念モデルを、技術的に中立(RDB/NoSQLなどに非依存)な「論理的な“あるべき姿”」 として詳細化すること。
・整合性(Consistency)の境界と、パフォーマンス(Use Case)の要件に基づき、データを論理的にグルーピングする「仮説」を構築する。
前提条件 (Preconditions)
・フェーズ1(概念モデル)が完了している。
・主要なユースケース(例:「注文を処理する」「プロフィール画面を表示する」)が明確になっている。
行うこと (Activities)
-
概念モデルのM:N関係を、中間テーブルを導入して解決する。
-
すべてのエンティティと属性を定義し、主キー(PK)と外部キー(FK)を定義してエンティティ間の参照整合性を明確にする。
-
(仮説構築)「どのエンティティ群が ライフサイクルを共にするか?」(=トランザクション境界)を分析し、仮説を立てる。(例:「
注文と注文明細は必ず同時に作成/更新/削除されるはずだ。」) -
「どのエンティティ群がユースケース上、常に一緒に使われるか?」
(例:「ユーザーとユーザー設定は常にJOINされるはずだ。」)
その後これらの分析に基づき、エンティティを 「論理的なデータ群」 としてグルーピングします。このときに、後述のコンポーネントの凝集原則を指針にします。
事後条件 (Postconditions)
・正規化(例:第三正規形)され、キーが定義された詳細な論理ER図。
・コンポーネント原則に基づきグルーピングされた 「論理データ群」 のリスト。
これらの論理グループとトランザクション境界は、「検証されるべき仮説」 であるため、境界位置が曖昧な場合には、この段階で明確にしておきましょう。
用いる適用原則 (Applied Principles)
CCP (閉鎖性共通の原則)
上記のフェーズ2.3の分析。
「同じ理由で、同時に変更(更新/削除)される」エンティティ(例:注文と注文明細)を
「トランザクション境界グループ」 としてグルーピングする。
CRP (共通再利用の原則)
上記のフェーズ2.4の分析。
「ユースケースで常に一緒に再利用(JOIN)される」エンティティ(例:ユーザーとユーザー設定)を 「ユースケース・グループ」 としてグルーピングする。
REP (再利用・リリース等価の原則)
(仕様変更が頻繁な場合)この論理グループのスキーマ定義をバージョン管理(例:v1.0)し、変更の影響範囲を明確にする。
DDD
CCPの分析は、DDDの 「集約(Aggregate)」 を定義するプロセスとほぼ同義。
事例 (Examples)
・CCPに基づき、Orders, OrderLineItems, OrderShippingDetailsを 「注文集約(Order Aggregate)」 論理グループとする。
・CRPに基づき、Products, Categories, ProductReviewsを 「商品カタログ(Product Catalog)」 論理グループとする。
注意点 (Cautions)
この段階でも、まだ
「物理的な配置」(どのDBサーバーに置くか、どのセグメントか)は、一切考慮しない。
「論理的な理想形」を追求しましょう。
フェーズ3:物理データモデリング (Physical Data Modeling)
目的 (Objective)
・「論理モデル」という理想を、「物理インフラ」という現実にマッピングすること。
・非機能要件(データ品質特性、コスト、セキュリティ、可用性)に基づき、論理グループを「共存させるか」「物理分離するか」 という、最も重要なトレードオフの意思決定を行う。
上記仮説を検証するための 「観測可能な(Observable)」 システムを構築する。
前提条件 (Preconditions)
・フェーズ2(論理モデル)が完了している。
・非機能要件(NFRs)が明確になっている(例:スケーラビリティ要件、セキュリティ要件、コスト要件)。
行うこと (Activities)
-
データベース技術(PostgreSQL, DynamoDBなど)を選定する。
-
論理データ型を、選定したDBの物理データ型(例:
VARCHAR(100),BIGINT)に変換する。 -
インデックス、パーティショニング、非正規化(パフォーマンスのための意図的な正規化崩し)などを設計する。
-
(多角的な脅威分析) フェーズ2で定義した「論理データ群」ごとに、以下のリスクを分析・評価する。
脅威A(整合性/レイテンシー)
「このCCPグループを物理分離した場合、結果整合性になり、トランザクションが組めなくなる。そのレイテンシー遅延はビジネス要件として許容できるか?」
脅威B(セキュリティ/爆発半径)
「コアドメインの論理グループと支援ドメインの論理グループを、コストのために物理的に共存させた場合、支援ドメインの侵害がコアドメインに及ぶ爆発半径を許容できるか?」
脅威C(スケーラビリティ/コスト)
「書き込み集中型の論理グループと読み取り集中型の論理グループを物理的に共存させた場合、リソース競合や非効率なスケール(コスト増) を許容できるか?」
最終決定
上記の分析に基づき、各論理グループの最終的な物理配置(どのDBインスタンス/ネットワークセグメントに置くか)を決定する。
システムに オブザーバビリティ(可観測性) を実装する。
(トランザクションログ、クエリログ、パフォーマンスメトリクスの収集)
事後条件 (Postconditions)
・物理ER図(DB固有)。
・デプロイメントモデル(配置図)とIaCの設計のインプットとなる、最終的な物理データアーキテクチャ。
・検証に必要なログやメトリクスを生成する、稼働中のシステム(MVPやv1)。
用いる適用原則 (Applied Principles)
YAGNI (You Ain't Gonna Need It)
フェーズ3.4の分析において、
「物理分離」のメリットがリスク(脅威B, C)を明確に上回らない限り、
「いったん分離せず、同じDBに共存させる」
という最もシンプルな選択をデフォルトとする。
脅威モデリング / リスク分析
上記の脅威A, B, Cのトレードオフを評価する。
TOC (制約理論)
どの脅威(例:レイテンシー)が、システム全体の最大の制約となるかを判断する。
データオブザーバビリティ
検証フェーズ(フェーズ4)のためのデータ収集を実装する。
事例 (Examples)
決定A(共存)
注文集約(論理グループ1)と商品カタログ(論理グループ2)は、JOINのレイテンシー(脅威A)が最大の制約であると判断。
爆発半径のリスク(脅威B)は許容し、同じPostgreSQLインスタンスに共存させる。
ただし、論理スキーマは分離しておく。
決定B(分離)
注文集約(論理グループ1)とアクセスログ(論理グループ3)は、スケーラビリティ要件(脅威C)が全く異なる。
注文集約はPostgreSQLに、アクセスログは別のClickHouse DBに物理分離する。
注意点 (Cautions)
ここでの決定は、後からの変更コストが最も高い。
「YAGNIに従いシンプルに始める」ことと、「将来の分離(リファクタリング)」を見越した論理設計(フェーズ2)を両立させることが重要。
フェーズ4:観測と検証 (Observability & Validation) - フィードバックループ
データモデリングを行って終わりではありません。
一度つくったデータモデルは、あくまでも仮説にすぎません。
必ず運用フェーズでそのモデルの検証を実データで行う必要があります。
目的 (Objective)
・稼働中のシステム(フェーズ3)から得られる
「実際のトランザクションログ」や「クエリメトリクス」という事実データ
に基づき、フェーズ2(論理モデル)で立てた「仮説」が正しかったかを検証する。
・アーキテクチャの 「隠れたボトルネック」や「設計のズレ」 を発見し、継続的な改善(リファクタリング)に繋げる。
前提条件 (Preconditions)
フェーズ3のシステムが稼働しており、観測可能なデータ(ログ、メトリクス)を生成している。
行うこと (Activities)
1. データ収集
オブザーバビリティツール(Datadog, Splunk, Prometheusなど)を用いて、トランザクションログやクエリパフォーマンスデータを収集する。
2. (仮説検証) CCPの検証
・仮説
「注文と注文明細はライフサイクルを共にする(CCP)はずだ」
・検証
トランザクションログを分析する。
「注文テーブルだけが更新され、注文明細が更新されていない(あるいはその逆の)トランザクションが、予期せず発生していないか?」
・発見
もし発生していれば、仮説は棄却され、論理モデル(フェーズ2)の「トランザクション境界」の定義が間違っていたことが証明されます。
3. (仮説検証) CRPの検証
・仮説
「ユーザーとユーザー設定は常に一緒にJOINされる(CRP)はずだ」
・検証
クエリログ(スロークエリなど)を分析する。
「ユーザーテーブルだけへのクエリが大量に発生していないか?」
「ユーザーとユーザー設定のJOINが、実はシステムの ボトルネック(TOCの制約) になっていないか?」
・発見
もしユーザーだけへのクエリが99%で、JOINがボトルネックになっているなら、仮説は棄却され、論理モデル(フェーズ2)の「ユースケース・グループ」の定義が間違っていたことが証明されます。
4. TOCに基づくボトルネック特定
Four Keysなどのメトリクスを監視し、システム全体の最大の制約(例:特定のクエリの遅延)を特定する。
事後条件 (Postconditions)
・検証済みの学習。
・フェーズ2(論理モデル)またはフェーズ3(物理モデル)への、具体的なリファクタリング・バックログ項目。
用いる適用原則 (Applied Principles)
アブダクション(仮説形成推論)
「観測された事実(ログ)」から、「最も可能性の高い原因(設計のズレ)」を推論する。
クリティカルシンキング
過去の自分たちの「仮説(論理モデル)」を、新しい「事実(ログ)」に基づいて客観的に精査する。
TOC (制約理論)
事実データに基づき、アーキテクチャの真のボトルネックを特定する。
Four Keys
変更障害率(CFR)やサービス復旧時間(MTTR)の悪化を、アーキテクチャの不整合のシグナルとして検知する。
事例 (Examples)
発見
「トランザクションログ(フェーズ4)を分析した結果、注文と決済(論理分離していた)が、99%のケースで5ミリ秒以内に同時に更新されていることが判明した。」
アクション
「論理モデル(フェーズ2)の仮説を見直し、この2つは単一のトランザクション境界(CCP)としてグルーピングすべきだ」というリファクタリング計画を立てる。
注意点 (Cautions)
フィードバックループ
このフェーズ4は「終わり」ではなく、フェーズ2または3への「ループバック」です。
このサイクル(設計→実装→観測→検証→再設計)を回し続けることこそが、変化に強いデータアーキテクチャのためには必須です。
2つの原則のトレードオフ
C.C.P. (Common Closure Principle)」とC.R.P. (Common Reuse Principle)の2つは、アーキテクチャの「安定性」と「変更容易性」という、相反する2つの力をどうバランスさせるかを示す、密接なトレードオフの関係にあります。
たとえば、コアドメイン内のデータモデルは、変更容易さが求められますが、
汎用ドメインの内部のには、で安定さが強く求められます。
ドメインの特性を理解した上で、どちらの原則をより強く満たす必要がありそうか
を考え続けましょう。
C.C.P. (閉鎖性共通の原則)
「変更」 に焦点を当てます。
「同じ理由で、同時に変更される」コンポーネントは、1つのモジュールに「集める (Close)」
べきです。
目的
ある変更(例:ビジネスロジックの変更)を行う際、触るべきファイルが1つのモジュール(例:1つのマイクロサービス)にまとまっていると、変更が容易になります。
C.R.P. (共通再利用の原則)
・「再利用」 に焦点を当てます。
「一緒に(共通して)再利用される」コンポーネントだけを、1つのモジュールに「集める」
べきです。(逆説的に言えば、一緒に使われないものは入れるなということ)
目的
あるモジュール(例:common-utilsライブラリ)に、使わない機能(例:ImageConverter)が大量に含まれていると、そのモジュールに依存する全サービスが、不要な機能(ImageConverter)にまで依存することになります。
2つの原則の「トレードオフ」
CCPとCRP、この2つの原則は、しばしば対立します。
・CCP.はモジュールを「大きく」しようとする力が働きます。(関連する変更をすべて集めたいため)
・CRP.はモジュールを「小さく」しようとする力が働きます。(不要な依存を避けたいため)
例: OrderService (注文サービス)
・createOrder (注文作成)
・cancelOrder (注文キャンセル)
・calculateShippingFee (送料計算)
C.C.P.の視点
「送料計算」のロジックは、「注文」というビジネスルールの一部です。
「注文」に関する仕様変更があれば、「送料計算」も一緒に変更される可能性が非常に高いです。
したがって、CCPに従うと、
calculateShippingFeeはOrderServiceモジュールに「含める」べき
です。
C.R.P.の視点
しかし、もしShippingService (配送サービス) も、見積もりのためにcalculateShippingFee (送料計算) を 「再利用」 したいとしたらどうでしょう?
CRPに従うと、ShippingServiceはOrderService全体に依存しなければならず、
不要なcreateOrderやcancelOrderのロジックにまで依存してしまいます。
これはCRP違反です。
結論 (どう解決するか)
このトレードオフは、「依存関係がどちらの方向に向かうべきか」 で解決します。
C.C.P.を優先する (推奨)
calculateShippingFeeは、CCPに従いOrderServiceに含めます。
ShippingServiceがOrderServiceに依存します。
C.R.P.を優先する (トレードオフ)
calculateShippingFeeをOrderServiceから分離し、新しいShippingFeeCalculatorという独立したモジュール(ライブラリ)として作成します。
そして、OrderServiceとShippingServiceの両方が、この新しいShippingFeeCalculatorに依存するようにします。
どちらが正しいかは、「変更の頻度」と「再利用の強さ」 を天秤にかけて決定します。
ケース1
もし「送料計算」ロジックがOrderServiceと密接不可分で、変更が頻繁なら、CCPを優先します。
ケース2
もし「送料計算」ロジックが安定的で、多くのサービスから広く再利用されるなら、CRPを優先します。
一般論
一般的に、CCP(変更容易性)は CRP(再利用性)よりも優先されることが多いです。
不要な再利用のためにコンポーネントを細かく分割しすぎると、CCPが崩壊し、
たった1つのビジネス変更のために多数のモジュールを修正しなければならなくなる
ためです。