Web●●●○○

GraphQL

ぐらふきゅーえる

クライアントが必要なデータの形を宣言して取得するAPIのクエリ言語。過不足取得を解消する。

概要

GraphQL は、Facebook(現 Meta)が開発した API のためのクエリ言語です。最大の特徴は、「どんなデータを、どんな形で欲しいか」をサーバではなくクライアントが宣言することにあります。クライアントは欲しいフィールドだけを列挙した問い合わせ(クエリ)を送り、サーバはそれと同じ形の JSON を返します。注文書と納品物の形が一致する、と言い換えられます。

登場する典型的な場面は、1つのバックエンドを多様な画面が使うサービスです。スマホアプリの一覧画面は名前とサムネイルだけ、Web の詳細画面は投稿履歴まで — 画面ごとに欲しいデータの形が違うとき、GraphQL ならサーバ側に画面専用の口を増やすことなく、各クライアントが自分に必要な形を自分で組み立てられます。

なぜ生まれたか

REST API では、リソースごとに用意されたエンドポイントが返すデータの形をサーバが決めます。ここから2つの慢性的な不満が生まれました。ひとつはオーバーフェッチ(over-fetching)— 一覧画面に名前だけ欲しいのに、プロフィール全項目が返ってくる無駄。もうひとつはアンダーフェッチ(under-fetching)— 「ユーザー情報 → その投稿一覧 → 各投稿のコメント」のような入れ子のデータを揃えるために、HTTP リクエストを何往復も重ねる手間です。回線の遅いモバイル環境では、この往復と無駄なデータ量がそのまま体感速度の悪化になります。

Facebook はまさにこの問題 — 多様な画面を持つモバイルアプリと巨大なデータグラフ — に直面し、2012年に社内で GraphQL を開発、2015年に公開しました。「サーバがデータの形を決める」から「クライアントが形を宣言し、サーバは能力(スキーマ)を公開する」への転換が、この技術の本質です。

詳細

スキーマ・クエリ・リゾルバの三点セット

GraphQL のサーバは、まずスキーマで「どんな型があり、どこからどこを辿れるか」を型システムとして定義します。クライアントはスキーマの範囲内でクエリを書き、サーバ側では各フィールドに対応するリゾルバ(そのフィールドの値を実際に取ってくる関数)がデータベースや他の API からデータを集めます。エンドポイントは通常1つだけで、REST のように URL を増やす代わりに、スキーマという型付きの地図を1枚公開する構図です。

クライアントクエリ = 欲しい形の宣言user {nameposts { title }}GraphQL サーバスキーマ型の地図・検証リゾルバ群フィールドごとに値を取ってくる関数データベース既存の REST API別サービスクエリ同じ形のJSONエンドポイントは1つ。応答はクエリと相似形の JSON になる
スキーマ・クエリ・リゾルバの関係 — クエリの形がそのまま応答の形になる

1リクエストで入れ子のデータを取る

REST なら「ユーザー取得 → 投稿一覧取得 → 各投稿のコメント取得」と数往復かかる問い合わせが、GraphQL では1つのクエリにまとまります。

query {
  user(id: "42") {
    name
    posts(limit: 3) {
      title
      comments { body }
    }
  }
}

応答はこのクエリと同じ入れ子構造の JSON になり、書いていないフィールドは一切含まれません。データを「表」ではなく「グラフ(つながりの網)」として捉え、必要な枝だけを辿る — 名前の由来でもあるこの発想が、往復の削減と過不足取得の解消を同時に実現します。取得だけでなく、更新は Mutation、サーバからのプッシュ配信は Subscription という形で同じ型システムの上に載っています。

N+1 問題と DataLoader

リゾルバは便利な反面、素朴に実装すると深刻な性能問題を招きます。投稿100件それぞれの著者フィールドを解決するとき、リゾルバが素直に動くと「投稿一覧に1回 + 著者取得に100回」のデータベース照会が発生します。これが有名な N+1 問題です。ORM でも同種の問題は起きますが、GraphQL ではクエリの形がクライアント次第なので、サーバ側で事前に JOIN を仕込んでおくという対処が通用しません。

定番の解決策が DataLoader です。1回のリクエスト処理中に発生した「著者IDで取得」の呼び出しをいったん溜めてから、「ID群をまとめて1回で取得」というバッチ照会に変換し、結果を各リゾルバに配り直します。あわせてリクエスト内での重複取得をキャッシュする役割も担います。GraphQL サーバを実装するなら、ほぼ必須の道具と考えてよいでしょう。

キャッシュの難しさと REST との使い分け

GraphQL の弱点としてよく挙がるのがキャッシュです。REST では「GET /users/42 の応答を URL 単位でキャッシュする」という HTTP 標準の仕組みや CDN がそのまま効きますが、GraphQL は単一エンドポイントへの POST でクエリごとに応答が変わるため、この経路が機能しません。代わりに Apollo Client などのクライアントライブラリが、応答をオブジェクトの ID 単位に分解して手元に正規化キャッシュする方式が主流ですが、その分クライアント側の複雑さは増します。ほかにも、悪意ある深い入れ子クエリでサーバを疲弊させられるため、クエリの深さやコストの制限が実務では必須です。

使い分けの目安はこうです。多様な画面・クライアントが1つのデータグラフを柔軟に読みたいとき、フロントエンドとバックエンドのチームが分かれていて「画面ごとの API 調整」がボトルネックになっているときは GraphQL が輝きます。一方、リソース構造が素直で HTTP キャッシュを活かしたい公開 API や、シンプルな CRUD には REST API の単純さが勝ります。銀の弾丸ではなく、「形の決定権をクライアントに移す」ことの利得がコストを上回る場面で選ぶ技術です。