RESTful APIで状態を表現するデータモデリング:ステータス管理と設計パターン
はじめに
多くの業務システムにおいて、データは時間の経過とともに様々な状態を変化させます。例えば、ECサイトの「注文」は「保留中」から「処理中」、「発送済み」、「完了」、「キャンセル」といった状態を経てライフサイクルを終えます。このような「状態を持つリソース」をRESTful APIでどのように表現し、モデリングするかは、APIの使いやすさ、保守性、そしてシステムの整合性を大きく左右します。
不適切な状態モデリングは、クライアント側に複雑な状態管理ロジックを強いたり、APIの変更が困難になったりする原因となります。本記事では、RESTful APIで状態を持つリソースを適切にモデリングするための基本的な考え方と、具体的な設計パターンについて解説します。
状態を持つリソースとは何か
「状態を持つリソース」とは、その属性値や許可される操作が、内部的な状態によって変化するリソースのことです。前述の注文リソースのように、状態が進むにつれてデータ構造の一部(例: 発送日が追加される)や、実行できるアクション(例: キャンセルできるのは特定の状態のときだけ)が変わります。
これらの状態は、システム内のビジネスロジックやワークフローに基づいて定義されます。API設計者は、この状態と状態遷移をどのようにリソース表現に落とし込み、クライアントに伝えるかを検討する必要があります。
APIにおける状態表現の基本的な考え方
RESTful APIでは、リソースの状態はリソースの「表現(Representation)」の一部としてクライアントに提示されるのが基本です。クライアントはリソースの表現に含まれる状態情報を読み取り、それに基づいてユーザーインターフェースを変化させたり、次に可能なアクションを判断したりします。
状態を表現する最も一般的な方法は、リソースのデータ構造に専用のフィールドを持たせることです。
具体的なデータモデリングパターン
状態を表現するための具体的なデータモデリングパターンをいくつか紹介します。
パターン1: 単一のステータスフィールド
最もシンプルで一般的なパターンです。リソースのトップレベルに、現在の状態を示す文字列フィールドを持たせます。
{
"id": "order-123",
"customer_id": "cust-456",
"amount": 5000,
"status": "pending", // 状態を示す文字列フィールド
"created_at": "2023-10-26T10:00:00Z"
}
注文が処理中に変わった場合:
{
"id": "order-123",
"customer_id": "cust-456",
"amount": 5000,
"status": "processing",
"created_at": "2023-10-26T10:00:00Z"
}
- メリット: シンプルで分かりやすい。現在の状態がフィールドを見れば一目でわかる。
- デメリット: 状態自体に関するメタデータ(例: いつ状態が変わったか、誰が変えたか)を含めにくい。状態が増えると文字列の管理が煩雑になる可能性がある。状態によって追加される情報(例: 発送日)をどう表現するかが次の課題となります。
パターン2: 状態に関連する追加情報を含む
リソースの状態によって、レスポンスに含めるべき情報が変わる場合があります。例えば、注文が「発送済み」になったら発送日や追跡番号が必要になります。
この場合、状態を示すフィールドはそのままに、状態に関連する情報を追加のフィールドとしてレスポンスに含める方法が考えられます。
方法A: 状態に関わらず全フィールドを返す(関連情報はnullの場合あり)
{
"id": "order-124",
"customer_id": "cust-789",
"amount": 8000,
"status": "processing",
"created_at": "2023-10-26T11:00:00Z",
"shipped_at": null, // 処理中なのでまだnull
"tracking_number": null, // 処理中なのでまだnull
"delivered_at": null
}
注文が「発送済み」になった場合:
{
"id": "order-124",
"customer_id": "cust-789",
"amount": 8000,
"status": "shipped",
"created_at": "2023-10-26T11:00:00Z",
"shipped_at": "2023-10-27T09:00:00Z", // 発送日が入る
"tracking_number": "TRK123456789", // 追跡番号が入る
"delivered_at": null
}
- メリット: クライアントは常に同じフィールドセットを期待できるため、パース処理がシンプルになります。後方互換性を保ちやすいです。
- デメリット: 多くのフィールドがnullになる状態がある場合、レスポンスが冗長になる可能性があります。
方法B: 状態に応じて動的にフィールドを追加・削除する
状態によって存在するフィールドを変える方法です。
注文が「処理中」の場合(方法Aと同じ):
{
"id": "order-124",
"customer_id": "cust-789",
"amount": 8000,
"status": "processing",
"created_at": "2023-10-26T11:00:00Z"
// shipped_at, tracking_number フィールドは存在しない
}
注文が「発送済み」になった場合:
{
"id": "order-124",
"customer_id": "cust-789",
"amount": 8000,
"status": "shipped",
"created_at": "2023-10-26T11:00:00Z",
"shipped_at": "2023-10-27T09:00:00Z",
"tracking_number": "TRK123456789"
}
- メリット: レスポンスがコンパクトになります。
- デメリット: クライアント側は、レスポンスに含まれるフィールドを状態に応じて動的に判断する必要があり、実装が複雑になる可能性があります。APIのスキーマ定義(OpenAPIなど)も状態ごとの定義が必要になるなど、管理が煩雑になりがちです。
一般的には、方法Aのようにフィールドを固定し、関連情報がない場合はnullとする方が、クライアントの実装コストを下げ、APIの安定性(後方互換性)を保ちやすい傾向があります。ただし、フィールド数が膨大になる場合は方法Bも検討価値があります。
パターン3: ネストされた状態オブジェクト
状態自体がより複雑な構造を持つ場合や、状態に関するメタデータ(いつ、誰が状態を変更したかなど)を保持したい場合に有効です。
{
"id": "order-125",
"customer_id": "cust-101",
"amount": 10000,
"state": { // 状態を表現するネストされたオブジェクト
"status": "shipped",
"updated_at": "2023-10-27T09:00:00Z",
"updated_by": "user-abc",
"shipping_info": { // 状態に紐づく詳細情報もネスト可能
"shipped_at": "2023-10-27T09:00:00Z",
"tracking_number": "TRK987654321"
}
},
"created_at": "2023-10-26T12:00:00Z"
}
- メリット: 状態に関するメタデータを構造化して管理しやすい。状態に紐づく詳細情報もまとめて表現できる。
- デメリット: 単一のステータスフィールドよりもレスポンスがやや冗長になります。
どのパターンを選択するかは、リソースの状態の複雑さ、状態に関連する情報の多さ、必要となるメタデータの有無などによって判断します。多くの場合は「パターン1 + パターン2(方法A)」の組み合わせで十分対応できるでしょう。
状態遷移の設計
リソースの状態をクライアントが変更できるようにするには、いくつかの設計方法があります。重要なのは、状態遷移のビジネスロジック(例: 支払いが完了していない注文は発送済みにできない)はサーバー側で厳格に管理することです。
方法A: 特定のアクション用エンドポイント
状態遷移をトリガーするアクションに対して、専用のエンドポイントを用意する方法です。
- 注文をキャンセルする:
POST /orders/{id}/cancel
- 注文を発送済みにする:
POST /orders/{id}/ship
- 注文を完了にする:
POST /orders/{id}/complete
これらのエンドポイントに対するリクエストボディには、アクションに必要な情報を含めます。例えば、発送アクションなら追跡番号や運送会社情報などです。
POST /orders/order-123/ship HTTP/1.1
Content-Type: application/json
{
"tracking_number": "TRK123456789",
"carrier": "Yamato Transport"
}
サーバー側では、現在の注文の状態を確認し、このアクションが許可されているか、必要な情報が揃っているかなどを検証します。成功すれば状態を更新し、更新後のリソース表現を返すのが一般的です(レスポンスコード 200 OK や 202 Accepted など)。
- メリット:
- APIのエンドポイントを見れば、そのリソースに対してどのような状態遷移アクションが可能かがある程度分かります。
- 状態遷移に関わるビジネスロジックを特定のエンドポイント内に凝集させやすいです。
- サーバー側での状態遷移のバリデーションが明確に行えます。
- デメリット: 状態遷移の数だけエンドポイントが増える可能性があります。
方法B: リソース更新(PATCH/PUT)による状態変更
リソース全体または一部を更新する標準的なエンドポイント(PUT /orders/{id}
や PATCH /orders/{id}
) を利用して、ステータスフィールドを更新する方法です。
PATCH /orders/order-123 HTTP/1.1
Content-Type: application/json
{
"status": "cancelled",
"reason": "Customer requested cancellation"
}
この場合も、サーバー側では現在の状態を確認し、指定された新しい状態への遷移が現在の状態から許可されているか(例: 既に発送済みの注文をキャンセルしようとしていないか)、リクエストに含まれる情報が適切かなどを検証する必要があります。
- メリット: CRUD操作に状態変更を統合できるため、エンドポイントの数は増えません。
- デメリット:
- API仕様だけでは、どのような状態遷移が可能で、どの状態からどの状態へ遷移できるのかが分かりにくい場合があります。
- 状態遷移に関する複雑なビジネスロジックが、汎用的なPUT/PATCHエンドポイント内に混在し、コードが複雑化する可能性があります。
- 遷移に必要な追加情報(例: 発送アクションにおける追跡番号)をリクエストボディに含める際に、PUT/PATCHの意味論と合わない場合があります(リソースの特定の部分を更新するのではなく、アクションをトリガーしているため)。
一般的には、状態遷移が明確なアクションであり、遷移に際して特定のビジネスロジックや追加情報が必要な場合は、方法A(アクション用エンドポイント)の方がAPIの意図が明確になり、サーバー側の実装も整理しやすいため推奨される傾向があります。単純な「アクティブ/非アクティブ」のような二値の状態変更であれば、方法Bも十分に考えられます。
アンチパターン
状態モデリングにおいて避けるべきアンチパターンをいくつか挙げます。
- 状態コードのマジックナンバー: 状態を「0: 保留中」「1: 処理中」のような数値コードで表現し、その意味をAPI仕様やドキュメントでしか確認できないようにすることです。クライアントやAPI利用者はコードの意味を都度調べる必要があり、可読性・保守性が著しく低下します。状態は意味のある文字列(例: "pending", "processing")で表現すべきです。
- 状態に応じた極端なレスポンス構造の変化: 状態によってJSONのトップレベルの構造が大きく変わるような設計です。例えば、注文リソースが「エラー状態」のときだけ、通常の注文データ構造ではなくエラー詳細だけの異なる構造を返すなどです。クライアント側でのパース処理が非常に複雑になり、エラー発生時などのハンドリングが困難になります。基本的なリソース構造は状態に関わらず一貫させるべきです。状態によって追加情報が変わる場合は、「パターン2: 状態に関連する追加情報を含む」で解説した方法などを検討してください。
- クライアントに状態遷移ロジックを委ねる: APIが単にリソースの状態を更新する機能だけを提供し、現在の状態から次にどの状態に遷移できるか、不正な遷移ではないかといった判断や制御を全てクライアント側のアプリケーションに任せる設計です。これはAPIではなくクライアントがビジネスロジックを持つことになり、サーバー側でのデータの整合性保証が難しくなります。状態遷移の制約は必ずサーバー側で管理・検証するべきです。
その他の考慮事項
- 状態遷移の制約: 特定の状態から遷移できるのは限られた状態のみである、といった制約はサーバー側で厳格に管理する必要があります。APIのドキュメントで可能な状態遷移を示すと、クライアント開発者にとって非常に役立ちます。
- ロールと権限: ユーザーのロールや権限によって、表示できる状態(例: 管理者のみが見られる非公開の状態)や、実行できる状態遷移アクションが異なる場合があります。これは認証・認可の範囲ですが、データモデリングやエンドポイント設計と密接に関連するため、設計段階で考慮が必要です。
- 冪等性: 状態変更のアクションが冪等であるべきか検討します。例えば、既にキャンセルされた注文に対して再度キャンセルAPIを呼び出した場合、エラーとするのか、あるいは成功(状態は変わらない)とするのか。後者であれば冪等性が保たれていると言えます。APIの性質やビジネス要件に応じて判断します。
まとめ
RESTful APIにおける状態を持つリソースのモデリングは、APIの品質に大きく影響します。状態はリソース表現の一部として明確に含め、可能であれば意味のある文字列フィールドを使用します。状態によってデータ構造が変わる場合は、一貫性を保ちつつ必要な情報を追加する方法を検討します。
状態遷移をトリガーするアクションは、専用のエンドポイントでサーバー側が制御するのが望ましいアプローチです。状態コードのマジックナンバーや、状態による極端なレスポンス構造の変化といったアンチパターンは避け、クライアントが扱いやすく、サーバー側で整合性を保証できる設計を目指すことが重要です。
適切な状態モデリングを行うことで、保守性が高く、理解しやすいAPIを構築することができます。