RESTful APIでリソースのバージョン履歴を扱うデータモデリング
はじめに
多くのシステムでは、データが時間と共に変化し、過去の状態を追跡したり、特定の時点のデータを確認したりする必要が生じます。文書管理システムにおけるドキュメントの改訂履歴や、設定管理システムにおける設定ファイルの変更履歴などがその代表例です。RESTful APIを設計する上で、これらの「リソースのバージョン履歴」をどのようにデータモデリングし、APIとして提供するかは重要な課題となります。
単に最新の状態を取得するだけでなく、特定の過去バージョンを取得したり、バージョン間の差分を確認したり、新しいバージョンを作成したりといった操作が必要になることがあります。これらの要件を満たすAPI設計は、クライアントがデータを正確に扱い、変更を管理するために不可欠です。
この記事では、RESTful APIでリソースのバージョン履歴を扱うためのデータモデリングのアプローチ、APIエンドポイントの設計パターン、具体的なリソース表現、そして設計における考慮事項について解説します。
リソースのバージョン履歴を扱うことの課題
リソースのバージョン履歴をAPIとして提供する場合、いくつかの設計上の課題があります。
- 過去バージョンの取得: 特定のリソースについて、最新ではない過去のバージョンをどのように指定し、取得できるようにするか。
- バージョンリストの提供: あるリソースの利用可能なバージョン一覧をどのように取得できるようにするか。
- 新しいバージョンの作成: 既存のリソースを編集して新しいバージョンとして保存する場合、どのようなAPI操作で実現するか。
- バージョン間の差分: 異なるバージョン間の変更内容(差分)をどのように取得できるようにするか。
- データ量の管理: リソースのバージョンが増えるにつれて、データ量が増大し、パフォーマンスへの影響が生じうる。これをどのように考慮するか。
これらの課題に対して、RESTfulの原則に基づきつつ、利用しやすく効率的なデータモデリングとAPI設計が求められます。
データモデリングの基本的な考え方
リソースのバージョン履歴をデータとしてモデル化する際、最も一般的なアプローチは、バージョンを元のリソースとは別のエンティティとして扱う、あるいは元のリソースにバージョン関連の情報を持たせることです。
例えば、「Document」というリソースがあり、そのバージョン履歴を管理する場合を考えます。
アプローチ1: バージョンをサブリソースとしてモデリングする
これは、バージョン履歴を元のリソース(Document)のサブリソースとして定義する考え方です。各バージョンは独立したリソースとしての性質を持ちつつ、常に親リソース(Document)に紐づきます。
- 親リソース:
Document
(特定のドキュメント全体を表す) - サブリソース:
Version
(Documentの特定のバージョンを表す)
Version
リソースは、そのバージョン自体の内容(例:ドキュメント本文)に加えて、そのバージョンに関するメタデータ(例:バージョン番号、作成日時、作成者、変更概要など)を持つことが考えられます。
データ構造のイメージ(JSON):
// Document リソースの例(最新バージョンや概要情報など)
{
"id": "doc-123",
"title": "プロジェクト計画書",
"current_version": 5,
"updated_at": "2023-10-27T10:00:00Z",
// ...その他のDocument全体のメタデータ...
}
// Version リソースの例(Document doc-123 のバージョン 3)
{
"id": "ver-456", // バージョン自体のユニークID
"document_id": "doc-123", // 親Documentへの参照
"version_number": 3,
"content": "プロジェクトの初期計画内容...", // このバージョンの具体的な内容
"created_at": "2023-09-15T09:00:00Z",
"created_by": {"id": "user-abc", "name": "山田太郎"},
"change_summary": "基本的な構成要素を定義"
}
このアプローチでは、元のリソースとバージョンという概念を明確に分離し、関連付けます。
アプローチ2: リソース自身にバージョン情報と内容を持たせる
このアプローチでは、各バージョンが独立したリソースとして存在し、それぞれのインスタンスがそのバージョンに対応する内容とメタデータを持ちます。この場合、特定の「最新バージョン」を示すポインタのようなものが別途必要になるかもしれません。
データ構造のイメージ(JSON):
// DocumentVersion リソースの例(バージョンを独立したリソースとして扱う)
{
"id": "ver-456", // このバージョンインスタンスのユニークID
"document_id": "doc-123", // 元のDocumentを識別するID
"version_number": 3,
"content": "プロジェクトの初期計画内容...", // このバージョンの具体的な内容
"created_at": "2023-09-15T09:00:00Z",
"created_by": {"id": "user-abc", "name": "山田太郎"},
"change_summary": "基本的な構成要素を定義"
}
そして、別に「最新バージョンがこれである」という情報を持つエンティティや、最新バージョンを取得するための別のリソース識別方法を設けることが考えられます。
RESTfulの文脈では、アプローチ1のように、バージョンを親リソースのサブリソースとして扱う方が、親リソースとバージョン履歴の関係性が直感的に理解しやすく、APIのパス設計もしやすいため推奨されることが多いです。以下では、このサブリソースモデルを前提にAPI設計を進めます。
APIエンドポイントの設計パターン
サブリソースモデルを採用した場合、APIエンドポイントは以下のようなパターンで設計することが考えられます。
1. 特定リソースの最新バージョンを取得
最も一般的な操作です。リソースのIDのみで最新バージョンを取得できるようにします。
GET /documents/{documentId}
レスポンス例: 最新バージョンの内容と、それがどのバージョンであるかを示す情報を含めるのが親切です。
{
"id": "doc-123",
"title": "プロジェクト計画書",
"version_number": 5, // 最新のバージョン番号
"content": "最新の計画内容...", // 最新バージョンの内容
"created_at": "2023-10-27T10:00:00Z", // 最新バージョンの作成日時
"created_by": {"id": "user-def", "name": "田中花子"}, // 最新バージョンの作成者
"change_summary": "最終調整"
}
注意点として、ここで返されるリソース構造は、バージョン一覧や特定バージョン取得のレスポンス構造と一部重複しますが、目的が「最新を取得する」ことにあるため、内容(content)を含めるのが一般的です。
2. 特定リソースのバージョン一覧を取得
あるリソースについて、利用可能なすべてのバージョンまたは一部のバージョンのリストを取得します。
GET /documents/{documentId}/versions
レスポンス例: バージョンごとのメタ情報のリストを返します。リストには内容(content)は含めず、バージョン番号、作成日時、作成者、変更概要などのサマリー情報のみを含めるのが一般的です。
[
{
"id": "ver-abc",
"version_number": 1,
"created_at": "2023-09-01T09:00:00Z",
"created_by": {"id": "user-abc", "name": "山田太郎"},
"change_summary": "初回作成"
},
{
"id": "ver-def",
"version_number": 2,
"created_at": "2023-09-10T14:30:00Z",
"created_by": {"id": "user-abc", "name": "山田太郎"},
"change_summary": "セクション追加"
},
// ...他のバージョン...
{
"id": "ver-xyz",
"version_number": 5,
"created_at": "2023-10-27T10:00:00Z",
"created_by": {"id": "user-def", "name": "田中花子"},
"change_summary": "最終調整"
}
]
大量のバージョンがある場合は、ページネーション (?page=...&size=...
) やフィルタリング (?created_by=...
)、ソート (?sort=version_number,desc
) などのクエリパラメータをサポートすることが望ましいです。
3. 特定の過去バージョンを取得
リソースのIDとバージョン指定子(バージョン番号など)を用いて、特定の過去バージョンを取得します。バージョン指定子には、連番のバージョン番号、UUID、タイムスタンプなどを使用することが考えられます。ここではバージョン番号を使用する例を示します。
GET /documents/{documentId}/versions/{versionNumber}
例: GET /documents/doc-123/versions/3
レスポンス例: 指定されたバージョンの内容とメタ情報を含みます。
{
"id": "ver-456", // このバージョンインスタンスのID
"document_id": "doc-123", // 親Document ID
"version_number": 3,
"content": "プロジェクトの初期計画内容...", // このバージョンの具体的な内容
"created_at": "2023-09-15T09:00:00Z",
"created_by": {"id": "user-abc", "name": "山田太郎"},
"change_summary": "基本的な構成要素を定義"
}
ここではパスパラメータ {versionNumber}
でバージョンを指定していますが、クエリパラメータ (/documents/{documentId}?version={versionNumber}
) で指定する方法も考えられます。パスパラメータはリソースの特定バージョンを独立したリソースとして表現する意図が強い場合に適しており、クエリパラメータはあくまで特定の時点の表現を「取得」するためのフィルタリングとして使用する意図が強い場合に適していると言えます。どちらの設計が良いかは、そのAPIがバージョンをどの程度独立したエンティティとして扱うかに依存します。サブリソースモデルを採用しているため、パスパラメータの方がよりRESTfulな表現に沿っていると考えられます。
4. 新しいバージョンを作成
既存のリソースの内容を更新し、それを新しいバージョンとして保存する操作です。これは、元のリソースに対してPOSTメソッドを使用し、リクエストボディに新しい内容と変更概要を含めることで表現できます。
POST /documents/{documentId}/versions
リクエストボディ例:
{
"content": "修正された計画内容...", // 新しいバージョンの内容
"change_summary": "期日を修正" // このバージョンの変更概要
}
レスポンス例:
成功した場合、新しく作成されたバージョンの情報(ID, バージョン番号, 作成日時など)を返します。201 Created
ステータスコードと共に、Location
ヘッダーに新しいバージョンリソースのURI (/documents/doc-123/versions/6
) を含めるのが標準的なRESTfulな応答です。
{
"id": "ver-newly-created",
"document_id": "doc-123",
"version_number": 6,
"created_at": "2023-10-28T11:00:00Z",
"created_by": {"id": "user-self", "name": "現在のユーザー"},
"change_summary": "期日を修正"
}
5. バージョン間の差分を取得
特定の2つのバージョン間の差分を取得するAPIエンドポイントです。
GET /documents/{documentId}/versions/{version1}/diff/{version2}
例: GET /documents/doc-123/versions/3/diff/5
(バージョン3からバージョン5への差分)
レスポンス例: 差分の表現方法は様々ですが、テキストベースの差分であればUnified Diff形式などが考えられます。構造化データ(JSONなど)の差分であれば、JSON Patchや独自の差分形式を用いることも可能です。
--- a/document-v3.json
+++ b/document-v5.json
@@ -1,6 +1,6 @@
{
"id": "doc-123",
- "title": "プロジェクト計画書",
+ "title": "改訂プロジェクト計画書",
"version_number": 3,
"content": "プロジェクトの初期計画内容...",
"created_at": "2023-09-15T09:00:00Z",
JSON Patch形式の例(もしリソース内容がJSONであれば):
[
{ "op": "replace", "path": "/title", "value": "改訂プロジェクト計画書" },
{ "op": "add", "path": "/status", "value": "Approved" }
]
どのような差分形式が適切かは、対象となるリソースの内容やクライアント側の処理要件に依存します。
考慮事項
- バージョン指定子の選択: バージョン番号(連番)は分かりやすいですが、並列編集がある場合は衝突管理が必要になることがあります。UUIDは衝突の心配はありませんが、順番が分かりにくいです。タイムスタンプは並列編集の際に同じ値になる可能性があり、精度に注意が必要です。ユースケースに応じて適切な指定子を選択してください。
- パフォーマンス: 大量のバージョン履歴を持つリソースの場合、バージョン一覧の取得や差分計算に時間がかかることがあります。リスト取得にはページネーションを必須とし、差分計算はサーバー側で効率的に行う仕組みが必要です。不要な古いバージョンをアーカイブまたは削除するポリシーも検討しましょう。
- セキュリティとアクセス制御: 過去の特定のバージョンへのアクセスも、最新バージョンと同様のアクセス制御が必要です。機密情報を含む過去バージョンへの不正アクセスを防ぐ設計が重要です。
- バージョン作成トリガー: 新しいバージョンを自動的に作成する条件(例:
PUT
またはPATCH
リクエストごとに自動作成)とするか、あるいはPOST /versions
のように明示的にバージョンを作成するAPIを提供するかの選択があります。明示的なAPIの方が、いつバージョンを作成するかをクライアント側が制御できるため、意図しないバージョンの増加を防ぎやすい場合があります。 - 最新バージョンの表現:
GET /documents/{documentId}
で常に最新バージョンを返す設計が一般的ですが、もし特定のバージョンを「現在有効なバージョン」とするようなユースケースがある場合は、その状態を別途モデリングする必要があります。
アンチパターン
- すべてのバージョンを一つのリソースに詰め込む: 特定のリソースの全バージョン内容を一つのAPIレスポンスで返すのは、データ量が膨大になり非効率です。必ずバージョンを個別のリソースとして扱える設計にしましょう。
- バージョン指定をURLの後半やボディに含める:
GET /documents/{documentId}
のボディにバージョン番号を含めたり、GET /versions/by-document/{documentId}?version={versionNumber}
のようにリソース階層を無視したりするのは、RESTfulの原則から外れます。リソースの識別はURLのパスで行うのが基本です。バージョンはサブリソースとしてパスで表現するか、取得時のフィルタリングとしてクエリパラメータで表現するのが適切です。 - 不変であるべきバージョンリソースを変更可能にする: 一度作成された特定のバージョン(例: バージョン3)の内容は、原則として不変であるべきです。これを
PUT
やPATCH
で変更できるようにしてしまうと、履歴としての信頼性が失われます。変更は必ず新しいバージョンとして作成する設計にしましょう。
まとめ
リソースのバージョン履歴をRESTful APIで扱うためには、バージョンを親リソースのサブリソースとしてデータモデリングし、対応するAPIエンドポイント(一覧取得、特定バージョン取得、新規バージョン作成、差分取得など)を適切に設計することが有効です。
- リソースのバージョンはサブリソース
/resources/{id}/versions
としてモデリングします。 - 特定のバージョンは
/resources/{id}/versions/{versionSpecifier}
のようにパスで識別できるようにします。 - バージョン履歴リストはサブリソースコレクションとして
/resources/{id}/versions
で取得できるようにします。 - 新しいバージョン作成は、コレクションへの
POST
で行います。 - パフォーマンス、セキュリティ、バージョン指定子の選択など、具体的なユースケースに応じた考慮が必要です。
バージョン履歴のモデリングは、データの変更可能性を適切に管理し、システムの監査性や復元性を高める上で重要な要素です。この記事で紹介したパターンや考慮事項が、保守性が高く、利用しやすいAPI設計の一助となれば幸いです。