DDDとORMのEntityを混同しないための考え方
2つの ”Entity”
ある種の ORM では RDB のテーブルスキーマモデルとなるクラスのことをEntity
と呼んでいます。例えば PHP のDoctrine
や TypeScript のTypeORM
などがそうです。
そういった ORM を採用したプロジェクトで DDD に取り組むとき困るのが用語の衝突です。ORM の Entity は RDB のための定義を含むため当然 DDD の Entity とは異なるのですが、なにぶん同じ名前なので混同してしまいがちです。
本記事では両者を混同せず扱うための考え方をまとめます。
Entity の定義
まずは定義から確認します。
DDD での定義
エヴァンス本の日本語訳から引用します。
主として同一性によって定義されるオブジェクトはエンティティと呼ばれる
Eric Evans. エリック・エヴァンスのドメイン駆動設計 (Japanese Edition) (Kindle Location 2331). Kindle Edition.
ORM での定義
Doctrine のドキュメントを参照します。
Entities are objects with identity. Their identity has a conceptual meaning inside your domain.
(Entity は同一性を持つオブジェクトです。その同一性はドメイン内での概念的な意義を持ちます。)
https://www.doctrine-project.org/projects/doctrine-orm/en/2.7/reference/working-with-objects.html#entities-and-the-identity-map
別のページには異なる説明もあります。
Every PHP object that you want to save in the database using Doctrine is called an Entity.
(Doctrine を通じてデータベースに保存されるすべての PHP オブジェクトを Entity と呼びます。)
https://www.doctrine-project.org/projects/doctrine-orm/en/2.7/reference/basic-mapping.html#creating-classes-for-the-database
前者の定義は本質的に DDD と同じ概念を指しています。Doctrine の Entity も同一性を持ったオブジェクトです。一方で後者の定義は RDB 側からの視点を含んでいます。
┌ アプリケーション側の視点 ┐
│ ↓ ├ DDD
ORM ┤ Entity ┘
│ ↑
└ RDB側の視点
つまるところ、Entity が同一性を持つことは共通ですがその同一性が、
- ドメインモデリングの要請によるものなのか
- RDB で管理する都合によるものなのか
の両方の可能性がありうるということです。
もしも Entity の同一性が RDB によるものだった場合どのような不都合があるのでしょうか。
ドメインモデルの歪み
インピーダンスミスマッチ
アプリケーション上と RDB で扱われるデータ構造の間にはインピーダンスミスマッチ
(impedance mismatch)と呼ばれる ”ずれ” があることが知られています。アプリケーション上でのデータ構造が一般的にネスト構造を取るのに対し、RDB 上では関係で結ばれたテーブルに分けられることによるずれです。
impedance mismatch: the difference between the relational model and the in-memory data structures.
Sadalage, Pramod J.; Fowler, Martin. NoSQL Distilled (p. 5). Pearson Education. Kindle Edition.
この ”ずれ” は下記の理由で普段は見過ごされがちなように思います。
- データベースとして RDB を選択することはデファクトスタンダードと言っていいくらい一般的である
- 同時に ORM を使用し ”ずれ” の接合を隠蔽することも一般的である
この ”ずれ” が及ぼす影響をフリマサービスのユーザーモデルを例に考えてみます。
アプリケーション上のデータ構造
まずはアプリケーション上で操作することだけを考慮しモデリングをしてみます。
アプリケーション上で自然に表現される Entity は JSON のようなネスト構造になります。
(簡略化のため JSON で記述しますが Entity クラスを表すものと考えてください)
// User
{
id: 1,
email: "[email protected]",
profile: {
// プロフィール
nickname: "seihmd",
introduction: "こんにちは",
avatar: "default.jpg",
},
address: "東京都...", // 配送先住所
}
RDB 上でのデータ構造
次にUser
データを RDB に保存することを考えます。
正規化の原則に従う場合、ネストされたデータは異なるテーブルに分けられることになります。
ORM を使う以上 Entity とテーブルは 1:1 になるので User
と Profile
を別々の Entity として定義し、RDB にはそれぞれに対応したテーブルを作成します。
// User
{
id: 1,
email: "[email protected]",
address: "東京都..."
profile: Profile
}
// Profile
{
id: 1,
userId: 1,
nickname: "seihmd",
introduction: "こんにちは",
avatar: "default.jpg"
}
user テーブル
id | address | |
---|---|---|
1 | foobar... | 東京都... |
profile テーブル
id | user_id | nickname | introduction | avatar |
---|---|---|---|---|
1 | 1 | seihmd | こんにちは | default.png |
この変更により、 プロフィール情報は同一性を獲得し晴れてProfile
Entity となりました。しかしその id は RDB のプライマリキーとしてのものであり、ドメインモデリングの要請として同一性が必要だったわけではありません。
もう一例として、ユーザーの配送先住所が複数登録できる仕様だった場合を考えてみます。
このとき address_1
, address_2
のようにテーブルのカラムを増やしていくのは目に見えたアンチパターンなので、正規化して別テーブルに分ける(いわゆる縦持ちにする)ことになると思います。
ということで配送先情報もUser
から独立して同一性を獲得し、Address
Entity となってしまいました。
// User
{
id: 1,
email: "[email protected]",
addresses: Address[],
profile: Profile
}
// Address
{
id: 1,
userId: 1,
value: "東京都...",
}
address テーブル
id | user_id | value |
---|---|---|
1 | 1 | 東京都... |
以上のように、ドメインモデリングの時点では Entity ではなかった情報が RDB を使うことで同一性を得て、 Entity として扱わざるをえなくなることがあります。以降ではそういった Entity を区別して ORMEntity と呼ぶことにします。
ORMEntity の弊害
ORMEntity はドメインモデリングでは独立していない情報が RDB によって独立した CRUD が可能になった存在です。ORM の提供する機能によって ORMEntity の単位で自由に CRUD を行うことができます。
しかしそういった行為はドメインルールを破るバックドアとなりかねません。
「1ユーザーが登録できる配送先住所は3つまで」のようなドメインルールから独立した操作を可能にし、 Entity 間の関係からドメインロジックを失わせ、操作を行うすべての箇所でルールを意識しなければならなくなります。
ドメインクラスにむやみに getter/setter をつける行為がドメイン貧血症
へとつながることに相似した形です。
ORMEntity はどのように扱うべきか
個人的に検討中なのですが、集約の手法をつかい ORMEntity の CRUD が集約ルートを介してのみ行われるようにするのが重要だと考えています。
個別の Entity として独立して扱うのは RDB 側の視点なのでアプリケーション側はドメインモデリングに忠実な(ネストしたオブジェクト構造をドメインロジックとして活かす)形を目指します。
アプリケーション側の視点
↓
User
Profile ┴ Address
------------------------
user profile address
↑
RDB側の視点
Discussion