Cache control mediante persisted queries
GraphQL normalmente opera vía POST, ejecutando todas las consultas contra un único endpoint y pasando los parámetros a través del cuerpo de la petición. La URL de ese único endpoint producirá respuestas distintas, lo que significa que no puede cachearse (al menos no usando la URL como identificador).
Así que la forma estándar de admitir cacheo en GraphQL es en la capa cliente, mediante el cliente Apollo y bibliotecas similares, que cachean los objetos devueltos de forma independiente entre sí, identificándolos por su ID global único.
(En contraste, al cachear en el servidor, normalmente usamos la URL como identificador, y cacheamos los datos de todas las entidades en la respuesta de forma conjunta.)
Pero esta solución tiene varias desventajas:
- La aplicación tiene que ejecutar más JavaScript en el lado cliente. Acceder al sitio web a través de un móvil de gama baja sufrirá un impacto en el rendimiento
- La aplicación se vuelve más compleja, y con más piezas en movimiento, ya que ahora también tenemos que preocuparnos por implementar la capa de cacheo
- No todo el mundo entiende JavaScript (p. ej.: el sitio web puede estar codificado en PHP), pero ahora lidiar con JS también se convierte en una responsabilidad
Una solución mucho mejor es usar cacheo HTTP. Veamos las precondiciones necesarias para que esto funcione.
Acceder a GraphQL vía GET
Usar cacheo HTTP significa que cachearemos la respuesta GraphQL usando la URL como identificador. Esto tiene 2 implicaciones:
- Debemos acceder al single endpoint de GraphQL vía
GET - Debemos pasar la consulta y las variables como parámetros de URL
Entonces, si el single endpoint es /graphql, la operación GET puede ejecutarse contra la URL /graphql?query=...&variables=....
Esto se aplica a la recuperación de datos del servidor (mediante la operación query). Para mutar datos (mediante la operación mutation), debemos seguir usando POST. No hay problema aquí, ya que las mutaciones siempre se ejecutan en fresco; no podemos cachear los resultados de una mutación, así que de todas formas no usaríamos cacheo HTTP con ella.
Este enfoque funciona (e incluso se sugiere en el sitio oficial), pero hay ciertas consideraciones a las que debemos prestar atención.
Codificar consultas GraphQL mediante parámetro de URL
Una consulta GraphQL normalmente abarcará varias líneas. Por ejemplo:
{
posts {
id
title
}
}Sin embargo, no podemos introducir esta cadena multilínea directamente en el parámetro de URL.
La solución es codificarla. Por ejemplo, el cliente GraphiQL codificará la consulta anterior así:
%7B%0A%20%20posts%20%7B%0A%20%20%20%20id%0A%20%20%20%20title%0A%20%20%7D%0A%7D
De acuerdo, esto funciona. Pero no se ve muy bien, ¿verdad? ¿Quién puede entender esa consulta?
Una de las virtudes de GraphQL es que sus consultas son muy fáciles de entender. Con algo de práctica, una vez que vemos la consulta, la entendemos inmediatamente. Pero una vez codificada, todo eso desaparece, y sólo las máquinas pueden comprenderla; el humano queda fuera de la ecuación.
Otra solución podría ser reemplazar todos los saltos de línea de la consulta con un espacio, lo que funciona porque los saltos de línea no añaden significado semántico a la consulta. Entonces, la consulta anterior puede representarse como:
?query={ posts { id title } }
Esto funciona bien para consultas sencillas. Pero si tienes una consulta realmente larga, abriendo y cerrando muchas { }, y añadiendo argumentos de campo y directivas, se vuelve cada vez más difícil de entender.
Por ejemplo, esta consulta:
{
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
}
}
}Se convertiría en esta consulta de una sola línea:
{ 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 } } }
Una vez más, ejecutar la consulta funcionará, pero no sabremos qué es lo que estamos ejecutando.
Y si la consulta también contiene fragmentos, entonces olvídalo por completo, no hay manera de darle sentido.
Las persisted queries acuden al rescate
Si pasar la consulta en la URL no resulta satisfactorio, ¿qué otra opción tenemos? Pues bien, ¡no pasar la consulta en la URL!
Este es el enfoque llamado "persisted query": Almacenamos la consulta en el servidor, y usamos un identificador (como un ID numérico, o una cadena única producida aplicando un algoritmo de hashing tomando como entrada la consulta) para recuperarla. Finalmente, pasamos este identificador como parámetro de URL, en lugar de la consulta.
Por ejemplo, la consulta podría identificarse con el ID 2908 (o un hash como "50ac3e81"), y entonces ejecutamos la operación GET contra la URL /graphql?id=2908. El servidor GraphQL recuperará entonces la consulta correspondiente a este ID, la ejecutará y devolverá los resultados.
Gato GraphQL lo hace aún más sencillo: una persisted query se implementa como un tipo de contenido personalizado, así que podemos crear una y publicarla como cualquier entrada normal, y el slug que elijamos (que por defecto se basa en el título que introduzcamos) se convertirá en su identificador. Las persisted queries hacen que implementar cacheo HTTP sea trivial.
Calcular el valor max-age
El cacheo HTTP funciona enviando la cabecera Cache-Control en la respuesta, con un valor max-age que indica la cantidad de tiempo que la respuesta debe cachearse, o no-store que indica no cachearla.
¿Cómo calculará el servidor GraphQL el valor max-age para la consulta, considerando que distintos campos pueden tener valores max-age diferentes?
La respuesta es: obtener el valor max-age para todos los campos solicitados en la consulta y averiguar cuál es el más bajo. Ese será el max-age de la respuesta.
Por ejemplo, supongamos que tenemos una entidad de tipo User. Siguiendo el comportamiento asignado a esta entidad, podemos asignar durante cuánto tiempo se puede cachear el campo correspondiente:
🛠 Su ID nunca cambiará ⇒ Damos al campo id un max-age de 1 año
🛠 Su URL se actualizará de forma muy aleatoria (si llega a ocurrir) ⇒ Damos al campo url un max-age de 1 día
🛠 El nombre de la persona puede cambiar de vez en cuando (p. ej.: para añadir un estado o decir "Milton (lleva máscara)") ⇒ Damos al campo name un max-age de 1 hora
🛠 El karma del usuario en el sitio puede cambiar en cualquier momento (p. ej.: después de que alguien vote positivamente su comentario) ⇒ Damos al campo karma un max-age de 1 minuto
🛠 Si consultamos los datos del usuario autenticado, la respuesta no puede cachearse en absoluto (independientemente del campo que estemos obteniendo) ⇒ El max-age debe ser no-store
Como resultado, la respuesta a las siguientes consultas GraphQL tendrá los siguientes valores max-age (para este ejemplo ignoramos el max-age del campo Root.users, pero en la práctica también se tendrá en cuenta):
| Consulta | Valor max-age |
|---|---|
| 1 año |
| 1 día |
| 1 hora |
| 1 minuto |
| no-store (no cachear) |
Crear la Cache Control List
Una vez que hemos identificado el max-age para cada campo, introducimos esta información mediante una Cache Control List:

Gato GraphQL calculará entonces automáticamente el valor max-age de la respuesta, y lo enviará de vuelta como la cabecera HTTP Cache-Control.