メインコンテンツへスキップ
バージョン: v18.0.0

永続化されたクエリ

Relayコンパイラは永続化されたクエリをサポートしています。これは以下の理由で役立ちます。

  • クライアントの操作テキストは、通常、実際のクエリ文字列よりも短いmd5ハッシュになります。これにより、クライアントからサーバーへのアップロードバイト数を節約できます。

  • サーバーは、クライアントによって実行できる操作を制限することでセキュリティを向上させるために、クエリの許可リストを設けることができるようになります。

クライアントでの使用法

persistConfigオプション

package.jsonrelay構成セクションで、"persistConfig"を指定する必要があります。

"scripts": {
"relay": "relay-compiler",
"relay-persisting": "node relayLocalPersisting.js"
},
"relay": {
"src": "./src",
"schema": "./schema.graphql",
"persistConfig": {
"url": "http://localhost:2999",
"params": {}
}
}

構成でpersistConfigを指定すると、次の処理が行われます。

  1. すべてのクエリおよびミューテーション操作テキストをmd5ハッシュに変換します。

    たとえば、persistConfigがない場合、生成されたConcreteRequestは以下のようになります。

    const node/*: ConcreteRequest*/ = (function(){
    //... excluded for brevity
    return {
    "kind": "Request",
    "operationKind": "query",
    "name": "TodoItemRefetchQuery",
    "id": null, // NOTE: id is null
    "text": "query TodoItemRefetchQuery(\n $itemID: ID!\n) {\n node(id: $itemID) {\n ...TodoItem_item_2FOrhs\n }\n}\n\nfragment TodoItem_item_2FOrhs on Todo {\n text\n isComplete\n}\n",
    //... excluded for brevity
    };
    })();

    persistConfigを使用すると、以下のようになります。

    const node/*: ConcreteRequest*/ = (function(){
    //... excluded for brevity
    return {
    "kind": "Request",
    "operationKind": "query",
    "name": "TodoItemRefetchQuery",
    "id": "3be4abb81fa595e25eb725b2c6a87508", // NOTE: id is now an md5 hash
    // of the query text
    "text": null, // NOTE: text is null now
    //... excluded for brevity
    };
    })();

  2. 指定されたurltextパラメータを含むHTTP POSTリクエストを送信します。paramsオプションを介して追加のリクエストボディパラメータを追加することもできます。

"scripts": {
"relay": "relay-compiler"
},
"relay": {
"src": "./src",
"schema": "./schema.graphql",
"persistConfig": {
"url": "http://localhost:2999",
"params": {}
}
}

ローカル永続化クエリ

次の構成を使用すると、operation_id => full operation textのマップを含むローカルJSONファイルを生成できます。

"scripts": {
"relay": "relay-compiler"
},
"relay": {
"src": "./src",
"schema": "./schema.graphql",
"persistConfig": {
"file": "./persisted_queries.json",
"algorithm": "MD5" // this can be one of MD5, SHA256, SHA1
}
}

理想的には、このファイルをデプロイ時にサーバーに送信して、サーバーが受信する可能性のあるすべてのクエリを認識できるようにします。それを行いたくない場合は、自動永続化クエリハンドシェイクを実装する必要があります。

トレードオフ

  • ✅ サーバーの永続化されたクエリデータストアがワイプされた場合、クライアントのリクエストを通じて自動的に回復できます。
  • ❌ キャッシュミスが発生すると、サーバーへの余分なラウンドトリップが発生します。
  • persisted_queries.jsonファイルをブラウザに送信する必要があり、バンドルサイズが増加します。

relayLocalPersisting.jsの実装例

クエリテキストをqueryMap.jsonファイルに保存する簡単な永続化サーバーの例を以下に示します。

const http = require('http');
const crypto = require('crypto');
const fs = require('fs');

function md5(input) {
return crypto.createHash('md5').update(input).digest('hex');
}

class QueryMap {
constructor(fileMapName) {
this._fileMapName = fileMapName;
this._queryMap = new Map(JSON.parse(fs.readFileSync(this._fileMapName)));
}

_flush() {
const data = JSON.stringify(Array.from(this._queryMap.entries()));
fs.writeFileSync(this._fileMapName, data);
}

saveQuery(text) {
const id = md5(text);
this._queryMap.set(id, text);
this._flush();
return id;
}
}

const queryMap = new QueryMap('./queryMap.json');

async function requestListener(req, res) {
if (req.method === 'POST') {
const buffers = [];
for await (const chunk of req) {
buffers.push(chunk);
}
const data = Buffer.concat(buffers).toString();
res.writeHead(200, {
'Content-Type': 'application/json'
});
try {
if (req.headers['content-type'] !== 'application/x-www-form-urlencoded') {
throw new Error(
'Only "application/x-www-form-urlencoded" requests are supported.'
);
}
const text = new URLSearchParams(data).get('text');
if (text == null) {
throw new Error('Expected to have `text` parameter in the POST.');
}
const id = queryMap.saveQuery(text);
res.end(JSON.stringify({"id": id}));
} catch (e) {
console.error(e);
res.writeHead(400);
res.end(`Unable to save query: ${e}.`);
}
} else {
res.writeHead(400);
res.end("Request is not supported.")
}
}

const PORT = 2999;
const server = http.createServer(requestListener);
server.listen(PORT);

console.log(`Relay persisting server listening on ${PORT} port.`);

上記の例では、完全なクエリマップファイルが./queryMap.jsonに書き込まれます。これを使用するには、package.jsonを更新する必要があります。

"scripts": {
"persist-server": "node ./relayLocalPersisting.js",
"relay": "relay-compiler"
}

ネットワーク層の変更

ネットワーク層のfetch実装を変更して、クエリパラメータの代わりにPOSTボディにIDパラメータ(例:doc_id)を渡す必要があります。

function fetchQuery(operation, variables) {
return fetch('/graphql', {
method: 'POST',
headers: {
'content-type': 'application/json'
},
body: JSON.stringify({
doc_id: operation.id, // NOTE: pass md5 hash to the server
// query: operation.text, // this is now obsolete because text is null
variables,
}),
}).then(response => {
return response.json();
});
}

サーバーでの永続化クエリの実行

クエリテキストではなく永続化されたクエリを送信するクライアントリクエストを実行するには、サーバーが各IDに対応するクエリテキストを検索できる必要があります。通常、これには、queryMap.json JSONファイルの出力をデータベースまたはその他のストレージメカニズムに保存し、クライアントによって指定されたIDに対応するテキストを取得することが含まれます。

さらに、relayLocalPersisting.jsの実装では、クエリをデータベースまたはその他のストレージに直接保存できます。

クライアントとサーバーのコードが1つのプロジェクトにあるユニバーサルアプリケーションの場合、クエリマップファイルをクライアントとサーバーの両方がアクセスできる共通の場所に配置できるため、これは問題ではありません。

コンパイル時のプッシュ

クライアントとサーバーのプロジェクトが別々であるアプリケーションの場合、1つのオプションとして、コンパイル時にクエリマップをサーバーがアクセスできる場所にプッシュする追加のnpm実行スクリプトを用意する方法があります。

"scripts": {
"push-queries": "node ./pushQueries.js",
"persist-server": "node ./relayLocalPersisting.js",
"relay": "relay-compiler && npm run push-queries"
}

./pushQueries.jsでできることのいくつかの可能性

  • サーバーリポジトリへのgit push

  • クエリマップをデータベースに保存します。

実行時のプッシュ

2つ目のより複雑なオプションは、サーバーが開始時にクエリIDを知らない状態で、実行時にクエリマップをサーバーにプッシュすることです。クライアントは、クエリマップを持たないサーバーに楽観的にクエリIDを送信します。次に、サーバーはクライアントから完全なクエリテキストを要求して、後続のリクエストのためにクエリマップをキャッシュできるようにします。これは、クライアントとサーバーがクエリマップを交換するために相互作用する必要がある、より複雑なアプローチです。

簡単なサーバーの例

サーバーがクエリマップにアクセスできるようになったら、マッピングを実行できます。解決策は、使用するサーバーとデータベースのテクノロジーによって異なるため、ここでは最も一般的で基本的な例のみを取り上げます。

express-graphqlを使用しており、クエリマップファイルにアクセスできる場合は、express-graphql-persisted-queriespersistedQueriesミドルウェアを使用して、直接インポートしてマッチングを実行できます。

import express from 'express';
import {graphqlHTTP} from 'express-graphql';
import {persistedQueries} from 'express-graphql-persisted-queries';
import queryMap from './path/to/queryMap.json';

const app = express();

app.use(
'/graphql',
persistedQueries({
queryMap,
queryIdKey: 'doc_id',
}),
graphqlHTTP({schema}),
);

persistConfig--watchの使用

persistConfig--watchオプションを同時に使用して、クエリマップファイルを継続的に生成することができます。これは、ユニバーサルアプリケーション、つまりクライアントとサーバーのコードが1つのプロジェクトにあり、開発中にlocalhostで両方を一緒に実行する場合にのみ意味があります。さらに、サーバーがqueryMap.jsonへの変更を認識するためには、サーバー側のホットリロードを設定する必要があります。この設定方法の詳細は、このドキュメントの範囲外です。


このページは役に立ちましたか?

サイトをさらに改善するために いくつかの簡単な質問に答えてください.