RESTful API データモデリング

RESTful APIで複合リソースを設計する:複数のデータを組み合わせて表現する方法

Tags: API設計, データモデリング, REST, 複合リソース, リソース設計

はじめに

RESTful API設計において、リソースは通常、システム内の個々のエンティティや概念(例: ユーザー、商品、注文など)に対応させて設計されます。これは、リソースに対するCRUD操作(作成、読み取り、更新、削除)を直感的に定義できるという点で、非常に強力なアプローチです。

しかし、実際のアプリケーション開発では、画面表示やレポート作成のために、複数のリソースに含まれる情報を組み合わせて一度に取得したいというニーズが頻繁に発生します。例えば、注文一覧画面で、各注文情報に紐づく顧客名や商品名を合わせて表示したい、といったケースです。

このような場合、単純に個々のリソースAPI(例: /orders, /customers, /products)をクライアント側で何度も呼び出し、それらを組み合わせてデータを整形するという方法も考えられます。しかし、APIコールの回数が増えたり、クライアント側のデータ結合ロジックが複雑になったりするなど、様々な非効率や課題が生じることがあります。

そこで重要になるのが、複数の基本リソースに含まれる情報を集約したり組み合わせたりして提供する「複合リソース」の設計です。この記事では、RESTful APIにおける複合リソースの考え方と、いくつかの設計パターン、そして設計時に考慮すべき点について解説します。

なぜ複合リソースが必要か? 課題の提起

基本的なリソースAPIだけでは、以下のような課題に直面することがあります。

  1. パフォーマンスの低下: 必要なデータを取得するためにN+1問題(一覧表示のために親リソースをN個取得し、それぞれの子リソースを取得するためにさらにN回APIコールするなど)が発生し、レイテンシが増大します。
  2. クライアント側の複雑化: 複数のAPIからのレスポンスを受け取り、クライアント側でデータを結合・整形するロジックが必要になります。これはクライアントアプリケーションの実装を複雑にし、保守性を低下させる要因となります。
  3. データの一貫性の問題: 複数のAPIコールを連続して行う間に、データが更新される可能性があり、取得したデータ間に一時的な不整合が生じるリスクがあります。

これらの課題を解決し、クライアントからの利用効率を高めるために、サーバー側でデータを集約・整形して提供する複合リソースの設計が有効になります。しかし、「どのような粒度で」「どのような構造で」複合リソースを設計すべきか、判断に迷うことも少なくありません。

複合リソースの基本的な考え方

複合リソースは、既存の個別の「基本リソース」を組み合わせたり、そこから派生・集約された情報を提供するリソースと考えることができます。その主な目的は、特定のユースケース(例えば、特定の画面表示やレポート出力)に必要なデータを、最小限のAPIコールで、クライアントが扱いやすい構造で提供することにあります。

複合リソースは必ずしもCRUDの全てをサポートする必要はありません。多くの場合、データの「読み取り(GET)」に特化したビューのような役割を果たします。データ更新が必要な場合は、その元となる個別の基本リソースに対するPUTやPATCHメソッドを使用することが一般的です。

複合リソースの設計にあたっては、以下の点を意識することが重要です。

複合リソースの設計パターン

複合リソースをAPIとして表現する方法にはいくつかのパターンがあります。代表的なものを紹介します。

パターン1: 新しいトップレベルリソースとして定義する

特定のユースケースのために、複数の基本リソースから必要な情報を集約し、新しい独立したリソースとして定義するパターンです。

例: 注文とそれに紐づく顧客情報、商品情報の一部を組み合わせた「注文詳細ビュー」のようなリソース。

{
  "orderId": "order-123",
  "orderDate": "2023-10-27T10:00:00Z",
  "totalAmount": 12500,
  "customer": { // 顧客情報をネスト
    "customerId": "cust-456",
    "customerName": "山田太郎"
  },
  "items": [ // 注文アイテム情報をネスト
    {
      "itemId": "item-a",
      "productId": "prod-xyz",
      "productName": "商品A",
      "quantity": 1,
      "unitPrice": 5000
    },
    {
      "itemId": "item-b",
      "productId": "prod-uvw",
      "productName": "商品B",
      "quantity": 3,
      "unitPrice": 2500
    }
  ]
}

この例では、注文(Order)、顧客(Customer)、商品(Product)、注文アイテム(OrderItem)といった基本リソースの情報が組み合わされています。顧客名や商品名は、元の基本リソースから取得された情報です。

パターン2: 既存リソースの拡張表現として定義する (Embedding/Expanding)

基本的なリソースを取得する際、関連する他のリソース情報もレスポンスに含める(埋め込む/展開する)ように指定するパターンです。HATEOASの一部として関連リソースへのリンクを含める方法とも関連しますが、ここでは直接データをペイロードに含めることに焦点を当てます。

クエリパラメータを使用して、どの関連リソースを埋め込むか制御することが一般的です。

例: 注文リストを取得する際に、各注文に紐づく顧客情報とアイテム情報を埋め込む。

[
  {
    "id": "order-123",
    "orderDate": "2023-10-27T10:00:00Z",
    "totalAmount": 12500,
    "customerId": "cust-456",
    "_embedded": { // 関連リソースを"_embedded"などのキーの下にまとめることが多い
      "customer": {
        "id": "cust-456",
        "name": "山田太郎"
      },
      "items": [
        {
          "id": "item-a",
          "orderId": "order-123",
          "productId": "prod-xyz",
          "quantity": 1,
          "unitPrice": 5000,
          "_embedded": { // アイテムに関連する商品情報をさらに埋め込むことも可能
            "product": {
              "id": "prod-xyz",
              "name": "商品A"
            }
          }
        },
        // ... 他のアイテム
      ]
    }
  },
  // ... 他の注文
]

この例では、_embedクエリパラメータで指定されたcustomeritemsが、元の注文リソースの構造内に_embeddedというキーを使って含まれています。

パターン3: ネストしたリソースとして定義する

親リソースの下に、特定の条件で集約またはフィルタリングされた子リソースや関連リソースのサマリーを表現するパターンです。

例: 特定の顧客の最新の注文サマリーリスト。

[
  {
    "orderId": "order-123",
    "orderDate": "2023-10-27T10:00:00Z",
    "totalAmount": 12500,
    "numberOfItems": 2 // 集約された情報
  },
  {
    "orderId": "order-456",
    "orderDate": "2023-10-20T09:15:00Z",
    "totalAmount": 8000,
    "numberOfItems": 1
  }
]

この例では、/customers/{customerId}という親リソースの下に/order-summariesというサブリソースを定義し、顧客ごとの注文サマリーを提供しています。サマリーなので、すべての注文アイテム情報は含まれていません。

データモデリングにおける考慮事項

複合リソースを設計する際には、いくつかの重要なデータモデリングの考慮事項があります。

  1. 必要な情報の粒度:

    • クライアントがその複合リソースを何に使うのかを具体的に考え、本当に必要な情報だけを含めます。
    • 過剰に情報を含めると、レスポンスサイズが増大し、セキュリティリスク(不要なデータの露出)やパフォーマンス劣化につながります。
    • 逆に情報が不足していると、結局クライアントが別のAPIを呼び出すことになり、複合リソースのメリットが薄れます。
    • 例えば、注文詳細ビューで、顧客の住所や電話番号が不要であれば含めない、といった判断が必要です。
  2. フラット化 vs ネスト化:

    • レスポンスのJSON構造を、関連データをネストさせるか、一部の情報をトップレベルにフラットに配置するか検討します。
    • ネスト化は元のリソース構造を反映しやすく、関連性が分かりやすいですが、構造が深くなりがちです。
    • フラット化はシンプルな構造になりますが、どの情報がどの元のリソース由来なのか分かりにくくなることがあります。
    • ユースケースに合わせて、どちらがクライアントにとって扱いやすい構造かを判断します。例えば、リスト表示で一覧性を重視するならフラット化が、詳細表示でデータ構造の関連性を重視するならネスト化が適している場合があります。
  3. 識別子 (ID) の扱い:

    • 複合リソースに、含まれる各エンティティ(元の基本リソース)のIDを含めるべきか検討します。
    • IDを含めることで、クライアントはその複合リソース内のデータを使って、個別の基本リソースAPIを呼び出すことが容易になります(例: 注文詳細から顧客IDを取得して、顧客詳細画面へ遷移するなど)。
    • 更新操作が必要な場合は、どのエンティティのどの情報を更新したいのかを特定するためにIDが不可欠です。しかし、複合リソースは基本的に読み取り専用ビューとすることが推奨されます。
  4. データの鮮度:

    • 複合リソースに含まれる複数のデータの鮮度について考慮が必要です。例えば、注文と在庫情報を組み合わせたリソースを提供する際、データの取得タイミングによって在庫数が変動している可能性があります。
    • リアルタイム性が求められる場合は、データの取得方法やキャッシュ戦略に注意が必要です。
  5. 更新操作への影響:

    • 繰り返しになりますが、複合リソースは原則として読み取り専用ビューとして設計することが推奨されます。
    • 複合リソースの一部(例: 注文詳細ビューに含まれる顧客名)を更新しようとすると、その更新が元の基本リソース(顧客リソース)にどのように反映されるべきか、ビジネスロジックが複雑になりがちです。
    • 更新操作が必要な場合は、対応する個別の基本リソースAPI (PUT /customers/{customerId}) を使用するようにクライアントに促すべきです。

アンチパターン

複合リソース設計における一般的なアンチパターンです。

実践的なヒント

まとめ

RESTful APIにおける複合リソースの設計は、クライアント側の効率性とシンプルさを向上させる上で非常に有効な手法です。しかし、その設計は単なる基本リソースの組み合わせではなく、特定のユースケースやデータ利用の目的に深く関連しています。

この記事で紹介したように、新しいトップレベルリソース、既存リソースの拡張表現、ネストしたリソースといったパターンや、情報の粒度、構造、更新操作といった考慮事項を踏まえ、複合リソースが本当に必要かを見極め、必要であれば利用シーンに最適な設計を選択することが重要です。

複合リソースを上手に活用することで、APIの利用者であるクライアント開発者にとって、より使いやすく効率的なAPIを提供できるでしょう。まずは小さな複合リソースから設計を始め、経験を積んでいくことをお勧めします。