RESTful API データモデリング

APIの部分更新(PATCH)を正しく設計する:データモデリングのアプローチ

Tags: RESTful API, データモデリング, PATCHメソッド, 部分更新, API設計

はじめに:なぜ部分更新(PATCH)のデータモデリングを考える必要があるのか

RESTful APIを設計する際、リソースの作成(POST)や全体更新(PUT)、削除(DELETE)、取得(GET)は比較的明確な操作として定義できます。しかし、リソースの一部だけを変更したいという要望は多くの場合に発生します。例えば、ユーザーのメールアドレスだけを変更したい、商品の在庫数だけを調整したいといったケースです。

このような部分的な更新には、通常HTTPメソッドのPATCHが用いられます。PUTがリソース全体を置き換える操作であるのに対し、PATCHはリソースに「差分を適用する」操作として定義されています。

PATCHメソッドは非常に便利ですが、PUTのようにリクエストボディの形式が明確に標準化されているわけではありません。どのような形式で「差分」を表現し、サーバー側でどのようにその差分を解釈してデータモデルに反映させるかは、API設計者が考慮すべき重要な点です。

特にデータモデリングの観点からは、以下の課題が生じがちです。

これらの課題を解決し、効率的で保守性の高いAPIを設計するためには、PATCHメソッドのためのデータモデリングのアプローチを体系的に理解することが不可欠です。この記事では、PATCHメソッドを使った部分更新をデータモデリングの視点からどのように設計すべきか、代表的なリクエストボディの形式や考慮事項、実践的なポイントを解説します。

PATCHメソッドの基本的な考え方とPUTとの違い

改めて、PUTPATCHの基本的な違いを確認しておきましょう。

重要なのは、PATCHが「差分を適用する」操作であるという点です。この「差分」をどのように表現するかが、データモデリングと密接に関わってきます。

データモデリング上の重要な考慮事項

PATCHリクエストを受け付けるAPIエンドポイントを設計する際、データモデルの観点から以下の点を考慮する必要があります。

更新可能なフィールドの定義

全てのリソースフィールドがクライアントから自由に変更されて良いわけではありません。例えば、以下のようなフィールドはPATCHの対象外とすべき場合が多いです。

APIの仕様として、どのフィールドがPATCH可能であるかを明確に定義し、ドキュメント化することが重要です。サーバー側では、PATCHリクエストボディに含まれるフィールドが更新可能リストに含まれているかチェックし、不正なフィールドが含まれていればエラーレスポンス(例: 400 Bad Request)を返すように実装します。

ネストした構造と配列の扱い

リソースのデータモデルがネストしたオブジェクトや配列を含む場合、部分更新はより複雑になります。

例えば、ユーザーリソースが以下のような構造を持つとします。

{
  "id": "user-123",
  "name": {
    "first": "Taro",
    "last": "Yamada"
  },
  "email": "taro.yamada@example.com",
  "address": {
    "city": "Tokyo",
    "street": "1-1 Chiyoda",
    "zip": "100-0001"
  },
  "tags": ["developer", "japan"],
  "createdAt": "2023-01-01T10:00:00Z"
}

null値の意味

多くのデータ形式(JSONなど)ではnullという値を表現できます。しかし、PATCHリクエストにおいて、リクエストボディの特定のフィールドの値がnullである場合、その意味合いは曖昧になり得ます。

この解釈も、採用するPATCHリクエストボディの形式によって異なります。API設計者は、nullが何を意味するかを明確に定義し、ドキュメントで指定する必要があります。

代表的なPATCHリクエストボディの形式

PATCHリクエストボディの形式に標準はありませんが、広く使われている代表的な形式がいくつか存在します。これらは「差分」の表現方法においてデータモデリングに影響を与えます。

1. JSON Merge Patch (RFC 7386)

JSON Merge Patchは、HTTP PATCHメソッドでJSONリソースを更新するためのシンプルな形式です。リクエストボディはJSONオブジェクトであり、既存のリソースのJSONオブジェクトにマージ(結合)する形で差分を表現します。

例: 元のリソース:

{
  "name": "Taro",
  "email": "taro.yamada@example.com",
  "address": {
    "city": "Tokyo",
    "zip": "100-0001"
  },
  "tags": ["developer"]
}

PATCHリクエストボディ (JSON Merge Patch):

{
  "email": "yamada.taro@example.com",
  "address": {
    "city": "Osaka"
  },
  "tags": ["developer", "jp"],
  "age": 30
}

適用後のリソース:

{
  "name": "Taro",
  "email": "yamada.taro@example.com",
  "address": {
    "city": "Osaka",
    "zip": "100-0001"
  },
  "tags": ["developer", "jp"],
  "age": 30
}

nameは変更なし、address内のzipはマージによって保持、tagsは全体が置き換え、ageが追加されています)

例2 (フィールド削除): 元のリソース(上の適用後リソースと同じ)

PATCHリクエストボディ (JSON Merge Patch):

{
  "address": null,
  "age": null
}

適用後のリソース:

{
  "name": "Taro",
  "email": "yamada.taro@example.com",
  "tags": ["developer", "jp"]
}

addressageフィールドが削除されました)

メリット: * 形式がシンプルで直感的。PUTリクエストのボディ構造と似ているため理解しやすい。 * 実装が比較的容易。

デメリット: * 配列の部分的な操作(要素の追加/削除/変更)が表現できない。 * フィールドを明示的にnull値に設定したい場合、JSON Merge Patchではそれがフィールド削除と解釈されるため表現できない。

2. JSON Patch (RFC 6902)

JSON Patchは、リソースに適用する一連の操作を記述するための形式です。リクエストボディは、適用すべき操作オブジェクトの配列になります。各操作オブジェクトは、操作の種類(op)、操作の対象を示すポインター(path、JSON Pointer形式)、および必要に応じて値(value)などを含みます。

例: 元のリソース:

{
  "name": {
    "first": "Taro",
    "last": "Yamada"
  },
  "email": "taro.yamada@example.com",
  "address": {
    "city": "Tokyo",
    "street": "1-1 Chiyoda"
  },
  "tags": ["developer", "japan"]
}

PATCHリクエストボディ (JSON Patch):

[
  { "op": "replace", "path": "/email", "value": "yamada.taro@example.com" },
  { "op": "replace", "path": "/address/city", "value": "Osaka" },
  { "op": "add", "path": "/tags/-", "value": "programmer" },
  { "op": "remove", "path": "/tags/1" }
]

/tags/-は配列の末尾に要素を追加するというJSON Pointerの特殊な記法です。/tags/1は元の"japan"タグを指します。)

適用後のリソース:

{
  "name": {
    "first": "Taro",
    "last": "Yamada"
  },
  "email": "yamada.taro@example.com",
  "address": {
    "city": "Osaka",
    "street": "1-1 Chiyoda"
  },
  "tags": ["developer", "programmer"]
}

(メールアドレスと都市が変更され、"programmer"タグが追加され、元の"japan"タグが削除されています)

メリット: * JSON構造に対するあらゆる種類の変更(追加、削除、置換、配列操作など)を非常に表現力豊かに記述できる。 * RFCとして標準化されている。 * null値を値として扱いたい場合も、"value": nullと指定することで表現可能(フィールド削除はremove操作で行う)。

デメリット: * JSON Merge Patchに比べて形式が複雑で、リクエストボディの作成・解釈にJSON Pointerなどの知識が必要となる。 * 多数のフィールドを変更する場合、リクエストボディが冗長になることがある。

3. カスタム形式

上記の標準形式を使用せず、APIの設計者が独自に定義した形式で差分を表現する方法です。

例: ユーザーリソースの一部更新を、変更したいフィールド名と値のリストで表現する。

{
  "updates": [
    { "field": "email", "value": "yamada.taro@example.com" },
    { "field": "address.city", "value": "Osaka" },
    { "field": "tags", "action": "add", "value": "programmer" },
    { "field": "tags", "action": "remove", "value": "japan" }
  ]
}

または、フィールド名をキーとするシンプルな形式で、ネストや配列操作は特別な記法や形式で表現するなど。

メリット: * 特定のドメインやユースケースに特化することで、最もシンプルで分かりやすい形式を設計できる可能性がある。 * 必要な操作だけを表現できる。

デメリット: * 標準形式ではないため、クライアントとサーバー間の仕様が密結合になり、クライアント実装者に学習コストがかかる。 * 汎用的なライブラリが存在しない場合が多く、実装コストがかかる。 * 形式の設計自体に考慮が必要で、後から拡張しにくい設計になるリスクがある。

どの形式を選択すべきか?

どのPATCHリクエストボディ形式を選択するかは、APIの要件、必要な表現力、シンプルさ、クライアント側の実装容易性などを総合的に考慮して判断します。

設計上のアンチパターン

PATCHメソッドを使ったデータモデリングで避けるべき代表的なアンチパターンを挙げます。

実践的な考慮事項

PATCH APIを設計し実装するにあたって、データモデリング以外にも考慮すべき実践的なポイントがあります。

まとめ

PATCHメソッドは、RESTful APIでリソースの部分更新を行うための強力な手段です。しかし、その柔軟性の高さゆえに、どのような「差分」をどのような形式で表現し、サーバー側でどのようにデータモデルに適用するかについて、設計者は明確な方針を持つ必要があります。

この記事では、PATCHメソッドのためのデータモデリングにおいて、更新可能なフィールドの定義、ネスト構造や配列の扱い、null値の解釈といった重要な考慮事項があることを解説しました。また、代表的なリクエストボディ形式としてJSON Merge PatchとJSON Patchを紹介し、それぞれの特徴や適したケース、そしてカスタム形式のメリット・デメリットについて触れました。

正しいPATCH APIを設計するためには、単に技術的な形式を選ぶだけでなく、ビジネス要件に基づいて更新可能なフィールドを適切に定義し、選択した形式のルールを明確にドキュメント化し、堅牢なバリデーションとエラーハンドリングを実装することが重要です。これらのデータモデリングと設計のアプローチを理解し実践することで、より効率的で保守性の高いRESTful APIを構築することができるでしょう。