Concepts, idées, stratégies
Concepts, idées, stratégiesCache control via les persisted queries

Cache control via les persisted queries

GraphQL fonctionne généralement via POST, en exécutant toutes les requêtes contre un unique endpoint et en passant les paramètres dans le corps de la requête. L'URL de cet unique endpoint produira des réponses différentes, ce qui signifie qu'il ne peut pas être mis en cache (du moins pas en utilisant l'URL comme identifiant).

Ainsi, la façon standard de prendre en charge le cache dans GraphQL se situe au niveau de la couche cliente, via le client Apollo et des bibliothèques similaires, qui mettent en cache les objets retournés indépendamment les uns des autres, en les identifiant par leur ID global unique.

(En revanche, lors de la mise en cache côté serveur, nous utilisons normalement l'URL comme identifiant, et nous mettons en cache les données de toutes les entités de la réponse ensemble.)

Mais cette solution présente plusieurs inconvénients :

  • L'application doit exécuter davantage de JavaScript côté client. Accéder au site web depuis un téléphone mobile d'entrée de gamme entraînera une perte de performances
  • L'application devient plus complexe, avec davantage de composants mobiles, car il faut maintenant également se préoccuper de l'implémentation de la couche de cache
  • Tout le monde ne comprend pas JavaScript (ex. : le site web peut être codé en PHP), mais traiter avec JS devient aussi une responsabilité

Une bien meilleure solution consiste à utiliser le cache HTTP. Voyons les conditions préalables nécessaires pour que cela fonctionne.

Accéder à GraphQL via GET

Utiliser le cache HTTP signifie que nous allons mettre en cache la réponse GraphQL en utilisant l'URL comme identifiant. Cela a 2 implications :

  1. Nous devons accéder au single endpoint de GraphQL via GET
  2. Nous devons passer la requête et les variables en tant que paramètres d'URL

Ainsi, si le single endpoint est /graphql, l'opération GET peut être exécutée contre l'URL /graphql?query=...&variables=....

Cela s'applique à la récupération de données depuis le serveur (via l'opération query). Pour modifier des données (via l'opération mutation), nous devons continuer à utiliser POST. Il n'y a pas de problème ici, car les mutations sont toujours exécutées à nouveau ; nous ne pouvons pas mettre en cache les résultats d'une mutation, donc nous n'utiliserions pas le cache HTTP avec elle de toute façon.

Cette approche fonctionne (et elle est même suggérée sur le site officiel), mais il y a certaines considérations auxquelles nous devons prêter attention.

Coder des requêtes GraphQL via un paramètre d'URL

Une requête GraphQL s'étend normalement sur plusieurs lignes. Par exemple :

{
  posts {
    id
    title
  }
}

Cependant, nous ne pouvons pas saisir cette chaîne multiligne directement dans le paramètre d'URL.

La solution est de l'encoder. Par exemple, le client GraphiQL encodera la requête ci-dessus comme ceci :

%7B%0A%20%20posts%20%7B%0A%20%20%20%20id%0A%20%20%20%20title%0A%20%20%7D%0A%7D

D'accord, cela fonctionne. Mais ce n'est pas très lisible, n'est-ce pas ? Qui peut comprendre cette requête ?

L'une des vertus de GraphQL est que ses requêtes sont très faciles à saisir. Avec un peu de pratique, une fois qu'on voit la requête, on la comprend immédiatement. Mais une fois encodée, tout cela disparaît, et seules les machines peuvent la comprendre ; l'humain est exclu de l'équation.

Une autre solution pourrait consister à remplacer tous les sauts de ligne de la requête par un espace, ce qui fonctionne car les sauts de ligne n'ajoutent aucune signification sémantique à la requête. Ainsi, la requête ci-dessus peut être représentée comme :

?query={ posts { id title } }

Cela fonctionne bien pour les requêtes simples. Mais si vous avez une requête vraiment longue, avec de nombreux { } ouvrants et fermants, et l'ajout d'arguments de champ et de directives, cela devient de plus en plus difficile à comprendre.

Par exemple, cette requête :

{
  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
    }
  }
}

Deviendrait cette requête en une seule ligne :

{ 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 } } } 

Encore une fois, l'exécution de la requête fonctionnera, mais nous ne saurons pas ce que nous exécutons.

Et si la requête contient également des fragments, alors oubliez-le complètement, il n'y a aucun moyen d'en avoir une vue d'ensemble.

Les persisted queries viennent à la rescousse

Si passer la requête dans l'URL n'est pas satisfaisant, quelle autre option avons-nous ? Eh bien, ne pas passer la requête dans l'URL !

C'est l'approche appelée « persisted query » : nous stockons la requête sur le serveur et utilisons un identifiant (comme un ID numérique, ou une chaîne unique produite en appliquant un algorithme de hachage avec la requête en entrée) pour la récupérer. Enfin, nous passons cet identifiant comme paramètre d'URL, à la place de la requête.

Par exemple, la requête pourrait être identifiée par l'ID 2908 (ou un hash comme "50ac3e81"), et nous exécutons alors l'opération GET contre l'URL /graphql?id=2908. Le serveur GraphQL récupérera alors la requête correspondant à cet ID, l'exécutera et retournera les résultats.

Gato GraphQL rend cela encore plus simple : une persisted query est implémentée en tant que type de contenu personnalisé, nous pouvons donc en créer une et la publier comme n'importe quel article ordinaire, et le slug que nous choisissons (qui est par défaut basé sur le titre que nous saisissons) deviendra son identifiant. Les persisted queries rendent l'implémentation du cache HTTP triviale.

Calcul de la valeur max-age

Le cache HTTP fonctionne en envoyant l'en-tête Cache-Control dans la réponse, avec une valeur max-age indiquant la durée pendant laquelle la réponse doit être mise en cache, ou no-store pour indiquer de ne pas la mettre en cache.

Comment le serveur GraphQL va-t-il calculer la valeur max-age pour la requête, étant donné que différents champs peuvent avoir des valeurs max-age différentes ?

La réponse est : obtenir la valeur max-age pour tous les champs demandés dans la requête, et déterminer laquelle est la plus faible. Ce sera le max-age de la réponse.

Par exemple, supposons que nous ayons une entité de type User. En suivant le comportement attribué à cette entité, nous pouvons définir pendant combien de temps le champ correspondant peut être mis en cache :

🛠 Son ID ne changera jamais ⇒ Nous donnons au champ id un max-age d'1 an

🛠 Son URL sera mise à jour très rarement (si jamais) ⇒ Nous donnons au champ url un max-age d'1 jour

🛠 Le nom de la personne peut changer de temps en temps (ex. : pour ajouter un statut, ou pour dire « Milton (porte un masque) ») ⇒ Nous donnons au champ name un max-age d'1 heure

🛠 Le karma de l'utilisateur sur le site peut changer à tout moment (ex. : après que quelqu'un ait voté positivement son commentaire) ⇒ Nous donnons au champ karma un max-age d'1 minute

🛠 Si nous interrogeons les données de l'utilisateur connecté, la réponse ne peut pas être mise en cache du tout (indépendamment du champ que nous récupérons) ⇒ Le max-age doit être no-store

Par conséquent, la réponse aux requêtes GraphQL suivantes aura les valeurs max-age suivantes (pour cet exemple, nous ignorons le max-age pour le champ Root.users, mais en pratique il sera également pris en compte) :

RequêteValeur max-age
{
  users {
    id
  }
}
1 an
{
  users {
    id
    url
  }
}
1 jour
{
  users {
    id
    url
    name
  }
}
1 heure
{
  users {
    id
    url
    name
    karma
  }
}
1 minute
{
  me {
    id
    url
    name
    karma
  }
}
no-store (ne pas mettre en cache)

Création de la Cache Control List

Une fois que nous avons identifié le max-age pour chaque champ, nous saisissons ces informations via une Cache Control List :

Définition d'une politique de cache control

Gato GraphQL calculera alors automatiquement la valeur max-age de la réponse et la renverra en tant qu'en-tête HTTP Cache-Control.