RESTful APIデータモデリングにおけるエンティティと値オブジェクト:違いと設計への影響
RESTful APIの設計において、どのようなデータをどのように表現するかは、APIの使いやすさ、保守性、そして将来的な拡張性に大きく影響します。特に、ドメイン駆動設計(DDD)の概念である「エンティティ」と「値オブジェクト」は、APIのデータモデリングを考える上で非常に重要な視点を提供してくれます。
これらの概念は、データベース設計やバックエンドのドメインモデルでは馴染みがあるかもしれませんが、それをAPIという外部とのインターフェースにどう落とし込むかで迷うことがあるかもしれません。
この記事では、エンティティと値オブジェクトの基本的な違いを改めて確認し、それがRESTful APIのデータモデリングにどのような影響を与えるのか、具体的な設計例を通して解説します。
エンティティとは何か?
エンティティは、ドメインの中で「識別子」によって区別される対象です。たとえ属性(プロパティ)が全く同じであっても、識別子が異なれば別のエンティティとして扱われます。
- 特徴:
- 一意な識別子(ID)を持つ。
- ライフサイクルを持つ(生成、変更、削除)。
- 属性は時間とともに変化しうる。
- 等価性は識別子によって判断される(識別子が同じなら同じエンティティ)。
例えば、「顧客」や「商品」、「注文」などは典型的なエンティティです。顧客Aさんと顧客Bさんは、名前や住所が同じでも、顧客IDが異なれば全く別の存在です。
RESTful APIにおいては、エンティティは通常、独立したリソースとして表現されることが多いです。
例:/customers/{customerId}
、/products/{productId}
値オブジェクトとは何か?
値オブジェクトは、その「属性値の組み合わせ」によって識別される対象です。識別子を持たず、その意味や等価性は属性値そのものによって決まります。属性値がすべて同じであれば、それらは同じ値オブジェクトとみなされます。
- 特徴:
- 識別子を持たない。
- 通常は不変(Immutable)であり、生成後に属性値を変更しない。
- 等価性はすべての属性値によって判断される(すべての属性値が同じなら同じ値オブジェクト)。
- 属性の集合として、ドメインにおける特定の概念や値を表現する。
例えば、「住所」、「金額」、「期間」などは典型的な値オブジェクトとして捉えることができます。住所「東京都渋谷区」は、どの顧客の住所であっても、その文字列の組み合わせが同じであれば同じ「東京都渋谷区」という値です。
RESTful APIにおいては、値オブジェクトは通常、エンティティリソースの属性としてインラインで表現されることが多いです。
例:顧客リソースの一部として住所データを含める
{
"id": "customer-123",
"name": "山田 太郎",
"email": "taro.yamada@example.com",
"address": {
"street": "渋谷1-2-3",
"city": "東京都",
"prefecture": "東京都",
"postalCode": "150-0002"
}
}
上記の例では、address
は識別子を持たず、その構成要素であるstreet
, city
などの値の組み合わせによって意味を持つため、値オブジェクトとして表現されています。
APIデータモデリングへの影響と設計判断
エンティティと値オブジェクトの違いを理解することは、APIリソースの粒度や構造を決定する上で重要です。
基本的な考え方
- エンティティ: 独立したライフサイクルを持ち、他のシステムやエンティティから参照される可能性があるものは、独立したリソース(例:
/users/{userId}
)またはネストされたリソース(例:/orders/{orderId}/items/{itemId}
)として表現することを検討します。リソースパスのセグメントとして識別子を用いるのが典型的です。 - 値オブジェクト: 特定のエンティティに強く紐づき、それ単独で独立した管理やライフサイクルを必要としないものは、そのエンティティリソースの属性として含めて表現することを検討します。
具体的な設計例と判断のポイント
例1:ユーザーと住所
-
ケースA:住所を値オブジェクトとしてユーザーリソースに含める ```json GET /users/{userId}
{ "id": "user-abc", "name": "テスト ユーザー", "billingAddress": { "street": "青山1-2-3", "city": "東京都", "country": "JP" }, "shippingAddress": { "street": "渋谷4-5-6", "city": "東京都", "country": "JP" } } ``` * 判断: 住所自体に独立したIDやライフサイクルが必要なく、常にユーザーとセットで扱われる場合。請求先住所と配送先住所のように、同じユーザーが複数の住所を持つが、それぞれの住所を単独で管理する必要がない場合など。 * メリット: ユーザー情報を取得する際に住所も同時に取得できるため、クライアントからのリクエスト回数を減らせます。データ取得がシンプルになります。 * デメリット: 住所だけを更新する操作が、ユーザーリソース全体に対する更新(PUT/PATCH)の一部となるため、住所更新のためだけにユーザーリソース全体の構造を知る必要が生じる可能性があります。また、同じ住所データが複数のユーザーで重複して格納される可能性があります(ただし、値オブジェクトなのでこれは問題とみなされないことが多いです)。
-
ケースB:住所を独立したリソース(エンティティ)として設計する ```json GET /addresses/{addressId} // 住所リソース
{ "id": "address-xyz", "street": "青山1-2-3", "city": "東京都", "country": "JP" // 関連するユーザーIDを含めるか、ユーザーリソースから参照するかは設計次第 }
GET /users/{userId} // ユーザーリソースから住所への参照を持つ
{ "id": "user-abc", "name": "テスト ユーザー", "billingAddressId": "address-xyz", // 住所ID "shippingAddressId": "address-pqr" // 別の住所ID } ``` * 判断: 住所自体に独立したIDが必要で、複数のユーザーが同じ住所を共有する場合や、住所リストを独立して管理・操作(登録、更新、削除など)するニーズがある場合。例えば、企業の所在地情報のように、ユーザーとは別の文脈で管理される可能性がある場合など。 * メリット: 住所を独立したリソースとして登録・更新・削除できるため、操作の責務が明確になります。同じ住所データを複数箇所で共有しやすくなります。 * デメリット: ユーザー情報と住所情報を取得するために複数のAPIコールが必要になる可能性があります。クライアント側で関連データを組み立てるロジックが必要になります。
例2:注文と注文項目
-
ケースA:注文項目を値オブジェクトのリストとして注文リソースに含める ```json GET /orders/{orderId}
{ "id": "order-789", "orderDate": "2023-10-27T10:00:00Z", "status": "Processing", "items": [ { // 注文項目1(値オブジェクト) "productId": "prod-aaa", "productName": "商品A", "quantity": 2, "unitPrice": 1000, "lineTotal": 2000 }, { // 注文項目2(値オブジェクト) "productId": "prod-bbb", "productName": "商品B", "quantity": 1, "unitPrice": 3000, "lineTotal": 3000 } ], "totalAmount": 5000 } ``` * 判断: 注文項目が注文に強く紐づいており、注文を構成する要素であり、注文項目単独での独立した管理(例: 個別の注文項目を後から追加・削除するAPIエンドポイントなど)の必要性が低い場合。 * メリット: 注文情報と注文項目がセットで取得できるため、クライアントは1回のAPIコールで必要なデータを全て取得できます。注文全体の整合性を保ちやすいです。 * デメリット: 個別の注文項目に対する操作(例: 数量変更)が、注文リソース全体への更新(PATCHなど)として設計される必要があり、リクエストボディの構造が複雑になる可能性があります。注文項目が多くなるとレスポンスサイズが大きくなります。
-
ケースB:注文項目を独立したリソース(エンティティ)として設計する ```json GET /orders/{orderId}/items/{itemId} // ネストされた注文項目リソース
{ "id": "item-xyz", // 注文項目自体のID "productId": "prod-aaa", "productName": "商品A", "quantity": 2, "unitPrice": 1000, "lineTotal": 2000 // 親である注文IDはパスに含まれている }
GET /orders/{orderId} // 注文リソースは項目のリストへのリンクを持つか、項目IDリストを持つ
{ "id": "order-789", "orderDate": "2023-10-27T10:00:00Z", "status": "Processing", "itemIds": ["item-xyz", "item-pqr"], // 注文項目のIDリスト // または HATEOAS リンク // "_links": { // "items": { "href": "/orders/order-789/items" } // } "totalAmount": 5000 }
`` * **判断:** 注文項目に独立したIDが必要で、個別の注文項目を後から追加・削除・変更するといった操作が多い場合や、注文項目自体が他のシステムから参照される可能性がある場合。 * **メリット:** 個別の注文項目に対するCRUD操作を独立したAPIエンドポイントとして設計できるため、操作の責務が明確で、API設計がよりRESTfulになります。特定の注文項目のみを効率的に取得・更新できます。 * **デメリット:** 注文情報と注文項目をまとめて表示するには、複数のAPIコールが必要になるか、
/orders/{orderId}?expand=items` のような拡張メカニズムが必要になります。
例3:金額や通貨
金額や通貨は、属性の集合(金額の値と通貨単位)として意味を持ち、通常は不変であるため、典型的な値オブジェクトです。
GET /products/{productId}
{
"id": "prod-aaa",
"name": "商品A",
"description": "良い商品です",
"price": { // 金額を値オブジェクトとして表現
"amount": 1000,
"currency": "JPY"
},
"stock": 50
}
金額を独立したリソースとして設計することは稀です。なぜなら、金額自体に独立したライフサイクルはなく、常に何らかのエンティティ(商品、注文、支払いなど)の属性として存在するからです。
設計上の考慮事項とアンチパターン
- 粒度: エンティティと値オブジェクトの区別は、APIリソースの適切な粒度を決定するのに役立ちます。あまりに細かすぎるリソース(すべてをエンティティにする)はAPIコールの増加とクライアント側の複雑さを招き、あまりに粗すぎるリソース(すべてを一つの巨大な構造に押し込める)は部分更新や再利用性を困難にします。
- 不変性: 値オブジェクトは原則として不変です。API設計においても、値オブジェクトとして表現した属性は、その値全体を置き換えることで更新する(例: PATCHで新しい住所オブジェクトを送信する)という考え方が自然です。値オブジェクト内の特定の属性だけをピンポイントで変更するAPIは、値オブジェクトの概念と矛盾し、設計を複雑にする可能性があります。
- データベーススキーマとの乖離: APIのデータモデルは、必ずしもバックエンドのデータベーススキーマと1対1である必要はありません。データベースでは関連テーブルになっているものが、APIでは値オブジェクトとしてインラインで表現されることもあります。APIは外部クライアントとの「契約」であるため、データベースの内部構造に引きずられすぎず、クライアントにとって最も理解しやすく使いやすい構造を目指すべきです。エンティティと値オブジェクトの区別は、このAPI独自のデータ構造を設計する上で役立ちます。
- アンチパターン:
- すべてをエンティティとして扱う: シンプルな属性の組み合わせであるべきものを、不必要に独立したリソースとして設計すると、APIが複雑になりすぎます。
- すべてをフラットな属性として扱う: エンティティの持つ構造的な情報を失い、関連性やドメインの意図が不明瞭になります。例えば、住所を
street1
,street2
,city
,postalCode
のようにフラットに持つよりも、address
というオブジェクトの中にまとめた方が、構造的で分かりやすいです。
まとめ
RESTful APIのデータモデリングにおいて、エンティティと値オブジェクトの概念を理解し、適切に使い分けることは、以下の点で重要です。
- 構造の明確化: リソースの粒度、関連性、そしてどのような情報がセットで意味を持つかが明確になります。
- 保守性の向上: 各リソースや属性の責務が明確になり、変更が必要になった場合の範囲を限定しやすくなります。
- APIの使いやすさ: クライアントが必要なデータを効率的に取得・操作できる構造を提供しやすくなります。
すべてのケースで明確な答えがあるわけではありませんが、「独立したライフサイクルが必要か?」「他のエンティティから参照されるか?」「属性の集合として意味を持つか?」といった問いを立てながら、エンティティとして独立させるか、値オブジェクトとして属性に含めるかを判断していくことが、質の高いAPI設計につながります。
チーム内でこれらの概念に対する共通理解を持ち、設計の基準を設けることも、一貫性のあるAPI開発を進める上で非常に重要です。