GraphQL永続化クエリ

はじめに

Persisted Queries(永続化クエリ)は、クエリ本文を毎回送らず、ハッシュ(通常はSHA-256)だけを送る方式。クライアントとサーバで事前にクエリを「登録(永続化)」しておくか、初回だけ本文を送り、以後はハッシュのみで呼び出します(Automatic Persisted Queries/APQ)。

メリットは3つ:①帯域削減(モバイルで効く)、②キャッシュ性向上(ハッシュ=安定キー)、③セキュリティallow-list/safelistで未知クエリを拒否)。本記事は仕組み→導入→CDN設計→セーフリスト運用→トラブル対処の順に、コードと意図で解説します。

1. 仕組みの全体像

1-1. 2段階リクエスト(APQの基本)

APQは「まずハッシュだけ送る→サーバが未登録なら本文も送って登録→以後はハッシュのみ」というプロトコル。本文送信は最初だけなので、以降はクエリが長くても転送量は一定です。

# クライアント送信(概念)
# 1回目: { extensions: { persistedQuery: { version:1, sha256Hash } }, operationName, variables }
# サーバ応答: 404/200(未登録なら "PersistedQueryNotFound")
# 2回目(未登録時のみ): 1回目 + query 本文 → サーバが保存
# 3回目以降: 1回目と同じでOK(キャッシュが効く)

意図:ハッシュを安定キーにすることで、CDNやHTTPキャッシュと相性が良く、エッジで高速応答が可能。

1-2. Safelist(許可リスト)運用

本番は「登録済みクエリのみ許可」が推奨。未知のハッシュや本文を含むリクエストは拒否。
CIでクエリ抽出→ハッシュ化→マニフェスト生成→サーバ配置を自動化しましょう。

2. サーバ導入(Apollo ServerのAutomatic Persisted Queries)

2-1. 最小実装(Node/Apollo Server)

import { ApolloServer } from '@apollo/server';
import { startStandaloneServer } from '@apollo/server/standalone';
import { ApolloServerPluginInlineTraceDisabled } from '@apollo/server/plugin/disabled'; // 任意
import { createPersistedQueryPlugin } from '@apollo/server-plugin-persisted-queries';
import crypto from 'crypto';

const typeDefs = `#graphql
  type Query { hello: String! }
`;
const resolvers = { Query: { hello: () => "world" } };

const server = new ApolloServer({
  typeDefs, resolvers,
  plugins: [
    ApolloServerPluginInlineTraceDisabled(),
    createPersistedQueryPlugin({
      hash: (s: string) =>
        crypto.createHash('sha256').update(s).digest('hex')
    })
  ],
});

startStandaloneServer(server, { listen: { port: 4000 } }).then(({ url }) => {
  console.log(`🚀 ${url}`);
});

意図createPersistedQueryPluginがAPQを処理。ハッシュはSHA-256でOK。プロダクションは外部キャッシュ(Redis等)で横展開に耐える。

2-2. クライアント(Apollo Client)設定

import { ApolloClient, InMemoryCache, HttpLink } from '@apollo/client';
import { createPersistedQueryLink } from '@apollo/client/link/persisted-queries';
import sha256 from 'crypto-js/sha256';

const client = new ApolloClient({
  link: createPersistedQueryLink({ sha256 })
    .concat(new HttpLink({ uri: '/graphql' })),
  cache: new InMemoryCache(),
});

意図:リンクが自動で「ハッシュのみ→未登録なら本文送信」にフォールバック。モバイル/低帯域に効く。

2-3. セーフリスト(固定化)モード

// 擬似例:unknown hash を拒否
const safelist = new Set(JSON.parse(fs.readFileSync('pq-manifest.json','utf8')));
app.post('/graphql', (req, res, next) => {
  const hash = req.body?.extensions?.persistedQuery?.sha256Hash;
  if (hash && safelist.has(hash)) return next();
  return res.status(400).json({ errors: [{ message: 'PersistedQueryNotAllowed' }]});
});

意図:本番は固定化、検証はAPQ(自動登録)など段階で使い分け。

3. CDNキャッシュ設計とHTTP

3-1. ハッシュをキーにキャッシュ

ハッシュ(sha256Hash)は安定キー。CDNのCache Keyに含める。GET化、またはPOSTでもボディをキーにできる基盤なら高ヒット可。

# 例:NGINXでPOSTボディ(hash)をキャッシュキーに反映(概念)
map $request_body $pqkey {
  "~sha256Hash\":\"([a-f0-9]{64})\"" $1;
  default "";
}
proxy_cache_key "$scheme$request_method$host$request_uri$pqkey";

意図:「本文」ではなく「ハッシュ」をキーにして高圧縮・高ヒットを狙う。

3-2. レスポンスのキャッシュ制御

Cache-Controlpublic, max-age(個人情報なし)。認可付きはprivate, no-store。モバイルはETag/If-None-Matchも有効。

3-3. 変数(variables)

変数でレスポンスが揺れる。不変IDフェッチ(詳細系)のみ積極キャッシュ、一覧/検索は短寿命が現実的。

4. モバイル/大規模運用の実践ポイント

4-1. モバイル最適化

APQで帯域削減、さらにHTTP/2多重化とgzip/br圧縮。エラー時の本文リトライは抑制、Backoffで電池消費を下げる。

4-2. スキーマ進化とロールアウト

ビルドでクエリ抽出→ハッシュ生成→pq-manifest.json出力→配信を自動化。サーバは青/緑で旧新を同時許可、ロールバック可能に。

4-3. Relay/他クライアント

Relayはビルド時に識別子生成でPersisted Query対応可。relay-compilerのプラグイン/ネットワーク層でハッシュ送信へ。

5. トラブルシューティング

5-1. PersistedQueryNotFound 多発

キャッシュ消失(再起動/スケールアウト)やハッシュ不一致。外部キャッシュ採用、ハッシュ計算(バイト順/改行)の差異を点検。

5-2. 未知の本文が届く

本番は本文付きリクエストを拒否。WAF/ルールでextensions.persistedQueryなしを遮断、Safelist限定に。

5-3. CDNヒット率が低い

キーにハッシュが入っていない、variables揺らぎ、Authorizationで共有キャッシュ無効、が典型。パブリック読み取り系を分離して最適化。

コード断片:ビルド時にクエリ→ハッシュ→マニフェスト

CIで.graphqlを集め、ハッシュ化してJSONへ。サーバのSafelistとして読み込みます。

// build-scripts/persist.js(概念)
const fs = require('fs'); const crypto = require('crypto'); const globby = require('globby');
(async () => {
  const files = await globby('src/**/*.graphql');
  const map = {};
  for (const f of files) {
    const query = fs.readFileSync(f, 'utf8').trim();
    const hash = crypto.createHash('sha256').update(query).digest('hex');
    map[hash] = query;
  }
  fs.writeFileSync('dist/pq-manifest.json', JSON.stringify(map, null, 2));
})();

意図:「hash → query」辞書を配布。APQはミス時のみ本文が必要だが、Safelist固定運用では辞書参照で本文不要にできる。

まとめ

Persisted Queriesは、GraphQLの帯域・キャッシュ・セキュリティを同時に改善。まずはAPQ導入→本番はSafelist固定→外部キャッシュとCDNでハッシュをキーに最適化、という段階導入が現実的。モバイルや大規模配信で特に効きます。


参考URL

採用情報 長谷川 横バージョン
SHARE
PHP Code Snippets Powered By : XYZScripts.com
お問い合わせ