RESTful APIのデータモデリング:複合キーを持つリソースの識別と操作
はじめに
多くの業務システムでは、データベースにおいてテーブルの主キーが複数の列で構成される「複合キー」が使用されています。例えば、注文明細のように「注文ID」と「明細番号」の組み合わせで一意になるデータや、中間テーブルのように「ユーザーID」と「グループID」の組み合わせで関係性を表すデータなどがこれにあたります。
このような複合キーを持つデータをRESTful APIで扱う際、どのようにリソースとして定義し、APIエンドポイントのURL設計やリクエスト・レスポンスのデータ構造を考慮すべきか、判断に迷うことがあるかもしれません。RESTfulな設計原則では、リソースは一意な識別子(ID)によって特定されることが基本とされます。この一意なIDという考え方と、複数の要素で構成される複合キーをどのように整合させるかが課題となります。
本記事では、RESTful APIにおいて複合キーを持つリソースをどのようにモデリングし、識別および操作するための具体的なアプローチについて解説します。
RESTful APIにおけるリソースの識別
RESTful APIの重要な原則の一つは、URI (Uniform Resource Identifier) を用いてリソースを一意に識別することです。通常、これは /resources/{id}
のような形式で表現され、{id}
にはリソースの単一のユニークな識別子が入ります。
複合キーを持つリソースの場合、この単一の識別子として表現することが難しい場合があります。例えば、OrderLine
というリソースが orderId
と lineNumber
の複合キーを持つとします。これをどのようにURLで表現すれば、RESTfulな原則に沿いつつ、リソースを一意に特定できるでしょうか。
複合キーを持つリソースのURL設計パターン
複合キーを持つリソースを識別するためのURL設計には、いくつかのパターンが考えられます。それぞれのパターンにはメリットとデメリットがあります。
パターン1: 複合キーの各要素をパスパラメータとして使用する
最もRESTfulなアプローチに近いとされるのが、複合キーを構成する要素をパスパラメータとしてURLに含める方法です。
例:/orders/{orderId}/lines/{lineNumber}
この設計は、親子関係や階層構造を持つリソース(この例では「注文」に対する「明細」)を表現するのに適しています。orderId
によって特定の注文が識別され、その子リソースである明細が lineNumber
によって識別されるという構造がURLから直感的に理解できます。
- メリット:
- RESTfulな階層構造を表現できる。
- リソースを一意に特定するための要素が明確である。
- パスパラメータはリソースの「識別子」の一部として適切に使用される。
- デメリット:
- 複合キーを構成する要素が多い場合、URLが長くなり、複雑になる可能性がある。
- 要素の順序に依存するため、クライアントは正確な順序でパラメータを指定する必要がある。
パターン2: 複合キーの各要素をクエリパラメータとして使用する
複合キーの要素をクエリパラメータとして渡す方法です。
例:/orderLines?orderId={orderId}&lineNumber={lineNumber}
このパターンは、リソースの「検索」や「フィルタリング」にクエリパラメータを使用するというRESTfulな原則からは外れます。クエリパラメータは、リソースの識別ではなく、既に識別されたリソースのコレクションに対する操作(絞り込み、ソートなど)に使用されるのが一般的です。
- メリット:
- URLのパス部分がシンプルになる。
- パラメータの順序に依存しない。
- デメリット:
- リソースの「識別」にクエリパラメータを使用するのは、RESTfulな原則から逸脱している。
- キャッシュ戦略などに影響を与える可能性がある。
- リソースを一意に特定するための主要な手段として扱うには不向きである。
パターン3: 複合キーの要素を組み合わせて単一のIDとする
複合キーの要素を何らかのルールで結合し、単一の文字列としてパスパラメータに含める方法です。
例:/orderLines/{combinedId}
(例: {orderId}-{lineNumber}
のように結合)
この方法では、APIの内部で {combinedId}
から元の orderId
と lineNumber
を復元する必要があります。結合ルールが明確で、かつ結合後の文字列が一意になることが前提となります。
- メリット:
- URLのパス部分がシンプルになる。
- RESTfulな単一IDでの識別という形式に近づけることができる。
- デメリット:
- 要素の結合・分解のルールを定義し、API内外で共有する必要がある。
- 結合されたID文字列の可読性が低下する可能性がある。
- 結合ルールによっては、URLエンコードなどの考慮が必要になる場合がある。
推奨パターン
複合キーを持つリソースを一意に識別する場合、パターン1 (/{key1}/{key2}/...
) が最もRESTfulな原則に沿っており、推奨されるアプローチです。特に、キー要素が少なく、リソース間に階層関係がある場合に有効です。
ただし、複合キーの要素が多数にわたる場合や、階層関係が複雑でない場合は、URLが不必要に長くなる、あるいは複雑になる可能性があります。その場合は、リソースの識別方法としてUUIDなどの単一の代理キーを導入し、複合キーはリソースのプロパティとして扱うという代替案も検討できます。しかし、本記事は「複合キーを持つリソースを複合キーで識別・操作する」というテーマに絞って解説を進めます。
CRUD操作とデータモデリング
URLでのリソース識別方法が決まったら、次にCRUD操作(作成、取得、更新、削除)におけるリクエスト・レスポンスのデータ構造をモデリングします。
ここでは、パターン1 (/orders/{orderId}/lines/{lineNumber}
) を採用し、OrderLine
リソースが orderId
(string), lineNumber
(integer), item
(string), quantity
(integer) というプロパティを持つと仮定します。
データの取得 (GET)
特定のリソースを取得するAPIエンドポイントは次のようになります。
GET /orders/{orderId}/lines/{lineNumber}
レスポンスボディには、取得した OrderLine
リソースの表現を含めます。複合キーである orderId
と lineNumber
も、リソースのプロパティとしてレスポンスに含めるのが一般的です。これにより、クライアントは取得したデータがどのリソースであるかを明確に認識できます。
レスポンスボディ (JSON例):
{
"orderId": "ORD12345",
"lineNumber": 1,
"item": "Laptop",
"quantity": 1,
"links": [
{
"rel": "self",
"href": "/orders/ORD12345/lines/1"
},
{
"rel": "order",
"href": "/orders/ORD12345"
}
]
}
links
プロパティはHATEOASの考え方を取り入れたもので、関連リソースへのナビゲーション情報を提供します。ここでは、自身のURI (self
) と親リソースである注文 (order
) へのリンクを含めています。
データの作成 (POST)
新しいリソースを作成する場合、通常は親リソースのコレクションエンドポイントに対してPOSTリクエストを行います。複合キーの一部(この例では orderId
)はURLで特定され、残りの複合キー要素 (lineNumber
) とリソースの他のプロパティはリクエストボディに含めます。
POST /orders/{orderId}/lines
リクエストボディには、作成したい OrderLine
の情報を指定します。orderId
はURLで指定されているためボディに含める必要はありませんが、混乱を避けるために含めることもあります(ただし、URLの値とボディの値が異なる場合はエラーとすべきです)。lineNumber
は通常、このリクエストで新規採番される場合もありますが、クライアントが指定する場合もあります。ここではクライアントが指定するケースを想定します。
リクエストボディ (JSON例):
{
"lineNumber": 2,
"item": "Mouse",
"quantity": 2
}
レスポンス:
成功時には 201 Created
ステータスコードを返し、Location
ヘッダーに作成されたリソースのURIを含めるのがRESTfulなプラクティスです。レスポンスボディには、作成されたリソースの完全な表現を含めることが推奨されます。
Locationヘッダー: /orders/ORD12345/lines/2
レスポンスボディ (JSON例):
{
"orderId": "ORD12345",
"lineNumber": 2,
"item": "Mouse",
"quantity": 2,
"links": [
{
"rel": "self",
"href": "/orders/ORD12345/lines/2"
},
{
"rel": "order",
"href": "/orders/ORD12345"
}
]
}
データの更新 (PUT / PATCH)
特定のリソースを更新する場合、識別子である複合キー全体をURLに含めて指定します。
PUT /orders/{orderId}/lines/{lineNumber}
PATCH /orders/{orderId}/lines/{lineNumber}
PUTリクエストはリソースの完全な置換を意図するため、リクエストボディには更新後のリソースの完全な状態(ただし、複合キー要素を除くのが一般的)を含めます。PATCHリクエストはリソースの部分的な更新を意図するため、リクエストボディには変更したいプロパティのみを含めます。
PUTリクエストボディ (JSON例):
{
"item": "Wireless Mouse",
"quantity": 2
}
(orderId
と lineNumber
はURLで指定済みのためボディには含めない)
PATCHリクエストボディ (JSON例):
{
"item": "Wireless Mouse"
}
レスポンス:
成功時には 200 OK
または 204 No Content
を返します。200 OK
の場合は、更新後のリソースの表現をレスポンスボディに含めることが一般的です。
データの削除 (DELETE)
特定のリソースを削除する場合も、識別子である複合キー全体をURLに含めて指定します。
DELETE /orders/{orderId}/lines/{lineNumber}
リクエストボディは通常不要です。
レスポンス:
成功時には 200 OK
(ボディ付き), 202 Accepted
, または 204 No Content
(ボディなし) を返します。リソースが完全に削除された場合は 204 No Content
が適切です。
考慮事項
- キー要素のデータ型とURLエンコード: 複合キーの要素が文字列の場合、スラッシュ (
/
) やクエスチョンマーク (?
) などの予約文字を含む可能性があります。これらの文字は適切にURLエンコードする必要があります。クライアント側、サーバー側双方でのエンコード・デコード処理が必要です。 - 複合キーの変更可否: 通常、複合キーはリソースを一意に識別するための固定値として扱われます。もし複合キー自体が変更される可能性があるデータであれば、その場合の更新操作をどう設計するか慎重な検討が必要です。基本的には、複合キーは「不変の識別子」として設計し、もし変更が必要な場合はリソースを一度削除してから新しい複合キーで再作成する、あるいは別途「キー変更」のような特殊な操作を定義するなどの方法が考えられます。
- キー要素が多い場合: 複合キーを構成する要素が非常に多い場合、パターン1 (
/{key1}/{key2}/...
) ではURLが過度に長くなり、実用的でなくなる可能性があります。このような場合は、前述のように単一の代理キー(UUIDなど)を導入し、API上はそちらを主たる識別子として使用することを検討することも有効です。複合キーはリソースの属性としてボディに含める形とします。
まとめ
RESTful APIで複合キーを持つリソースを扱う場合、最もRESTfulな設計原則に沿ったアプローチは、複合キーを構成する要素をURLのパスパラメータとして表現し、リソースを一意に識別することです。特に階層構造を持つデータに適しています。
- 識別:
/parent/{parentId}/child/{childId}
のようなURLパターンが推奨されます。 - CRUD操作:
- GET, PUT, PATCH, DELETEは、この複合キーをURLに含めて特定のリソースに対して実行します。
- POSTによるリソース作成では、親リソースのコレクションURL (
/parent/{parentId}/children
) に対してリクエストを行い、リクエストボディに残りの複合キー要素やその他のプロパティを含めます。
- データ構造: リソースの表現(リクエスト・レスポンスボディ)には、識別子である複合キーの要素もプロパティとして含めることで、クライアントがデータを扱いやすくなります。
複合キーを持つデータのRESTfulなモデリングは、単一キーの場合と比較して考慮すべき点が増えますが、本記事で解説したパターンと考慮事項を参考に、分かりやすく保守性の高いAPI設計を目指してください。