Persisted queriesによるキャッシュコントロール
GraphQLは通常、POSTを介して動作します。すべてのクエリを単一のエンドポイントに対して実行し、リクエストのボディにパラメータを渡します。その単一エンドポイントのURLは異なるレスポンスを返すため、キャッシュすることができません(少なくとも、URLを識別子として使用する場合は)。
そのため、GraphQLでキャッシュをサポートする標準的な方法は、Apolloクライアントやそれに類するライブラリを通じたクライアント層でのキャッシュです。これらは返されたオブジェクトをそれぞれ独立してキャッシュし、グローバルな一意IDで識別します。
(対照的に、サーバーでキャッシュする場合は通常URLを識別子として使用し、レスポンス内のすべてのエンティティのデータをまとめてキャッシュします。)
しかし、この解決策にはいくつかの欠点があります:
- クライアント側で実行するJavaScriptが増えます。ローエンドのスマートフォンでウェブサイトにアクセスするとパフォーマンスが低下します
- キャッシュ層の実装も考慮する必要があるため、アプリケーションがより複雑になり、動く部品が増えます
- JavaScript(例:ウェブサイトがPHPでコーディングされている場合)を全員が理解しているわけではありませんが、JSへの対応が責任になってしまいます
はるかに優れた解決策はHTTPキャッシュを使用することです。これを機能させるために必要な前提条件を見てみましょう。
GETによるGraphQLへのアクセス
HTTPキャッシュを使用するということは、URLを識別子としてGraphQLレスポンスをキャッシュするということです。これには2つの意味があります:
- GraphQLの単一エンドポイントに
GETでアクセスする必要があります - クエリと変数をURLパラメータとして渡す必要があります
したがって、単一エンドポイントが/graphqlの場合、GET操作はURL /graphql?query=...&variables=...に対して実行できます。
これはサーバーからデータを取得する場合(query操作)に適用されます。データを変更する場合(mutation操作)は、引き続きPOSTを使用する必要があります。ここに問題はありません。mutationは常に新たに実行されるため、mutationの結果をキャッシュすることはできず、HTTPキャッシュを使用することもないからです。
このアプローチは機能します(公式サイトでも推奨されています)が、いくつかの考慮事項に注意する必要があります。
URLパラメータによるGraphQLクエリのコーディング
GraphQLクエリは通常、複数行にわたります。例えば:
{
posts {
id
title
}
}しかし、この複数行の文字列をURLパラメータに直接入力することはできません。
解決策はエンコードすることです。例えば、GraphiQLクライアントは上記のクエリを次のようにエンコードします:
%7B%0A%20%20posts%20%7B%0A%20%20%20%20id%0A%20%20%20%20title%0A%20%20%7D%0A%7D
なるほど、これは機能します。しかし、見た目があまりよくないですね?このクエリの内容を理解できる人がいるでしょうか?
GraphQLの美徳の一つは、クエリが非常に把握しやすいことです。少し練習すれば、クエリを見た瞬間に理解できます。しかし、一度エンコードされると、その良さはすべて失われ、機械にしか解読できなくなります。人間は方程式の外に置かれてしまいます。
別の解決策として、クエリ内のすべての改行をスペースに置き換えることが考えられます。改行はクエリに意味的な意義を追加しないため、これは機能します。すると、上記のクエリは次のように表現できます:
?query={ posts { id title } }
これは単純なクエリには有効です。しかし、{ }を何度も開閉し、フィールド引数やディレクティブを追加するような非常に長いクエリがある場合、理解することがますます困難になります。
例えば、このクエリは:
{
posts(limit:5) {
id
title @titleCase
excerpt @default(
value:"No title",
condition:IS_EMPTY
)
author {
name
}
tags {
id
name
}
comments(
limit:3,
order:"date|DESC"
) {
id
date(format:"d/m/Y")
author {
name
}
content
}
}
}次のような1行のクエリになります:
{ posts(limit:5) { id title @titleCase excerpt @default(value:"No title", condition:IS_EMPTY) author { name } tags { id name } comments(limit:3, order:"date|DESC") { id date(format:"d/m/Y") author { name } content } } }
繰り返しになりますが、クエリの実行は機能しますが、何を実行しているのかがわかりません。
クエリにフラグメントも含まれている場合は、もはや完全に諦めるしかなく、理解する方法がありません。
Persisted queriesの登場
クエリをURLに渡すことが満足のいく方法でないとすれば、他にどんな選択肢があるでしょうか?それは、クエリをURLに渡さないことです!
これが「persisted query」と呼ばれるアプローチです:クエリをサーバーに保存し、識別子(数値IDや、クエリを入力としてハッシュアルゴリズムを適用して生成した一意の文字列など)を使って取得します。最後に、クエリの代わりにこの識別子をURLパラメータとして渡します。
例えば、クエリをID 2908(または"50ac3e81"のようなハッシュ)で識別し、URL /graphql?id=2908に対してGET操作を実行します。GraphQLサーバーはこのIDに対応するクエリを取得し、実行して結果を返します。
Gato GraphQLはこれをさらに簡単にします:persisted queryはカスタム投稿タイプとして実装されるため、通常の投稿と同様に作成して公開でき、選択したスラッグ(デフォルトでは入力したタイトルに基づく)が識別子になります。Persisted queriesにより、HTTPキャッシュの実装が非常に簡単になります。
max-age値の計算
HTTPキャッシュは、レスポンスにCache-Controlヘッダーを送信することで機能します。このヘッダーには、レスポンスをキャッシュすべき時間を示すmax-age値、またはキャッシュしないことを示すno-storeが含まれます。
異なるフィールドがそれぞれ異なるmax-age値を持てる場合、GraphQLサーバーはクエリのmax-age値をどのように計算するのでしょうか?
答えは:クエリで要求されたすべてのフィールドのmax-age値を取得し、その中で最も低い値を見つけます。それがレスポンスのmax-ageになります。
例えば、User型のエンティティがあるとしましょう。このエンティティに割り当てられた動作に従って、対応するフィールドがどのくらいの期間キャッシュできるかを割り当てられます:
🛠 そのIDは変わることがない ⇒ フィールドidにmax-ageとして1年を設定します
🛠 そのURLは滅多に更新されない(もしくは全く更新されない) ⇒ フィールドurlにmax-ageとして1日を設定します
🛠 人物の名前は時々変更される可能性がある(例:ステータスを追加したり、「Milton (マスク着用)」と記載する場合など) ⇒ フィールドnameにmax-ageとして1時間を設定します
🛠 サイトでのユーザーのカルマはいつでも変わりうる(例:誰かがコメントに賛成票を投じた後など) ⇒ フィールドkarmaにmax-ageとして1分を設定します
🛠 ログインユーザーのデータを照会する場合、レスポンスはいかなるフィールドを取得していても、まったくキャッシュできない ⇒ max-ageはno-storeでなければなりません
結果として、以下のGraphQLクエリへのレスポンスは以下のmax-age値を持ちます(この例ではRoot.usersフィールドのmax-ageは無視しますが、実際には考慮されます):
| クエリ | max-age値 |
|---|---|
| 1年 |
| 1日 |
| 1時間 |
| 1分 |
| no-store(キャッシュしない) |
Cache Control Listの作成
各フィールドのmax-ageを特定したら、Cache Control Listを通じてこの情報を入力します:

Gato GraphQLはその後、レスポンスのmax-age値を自動的に計算し、Cache-Control HTTPヘッダーとして返します。