Concetti, idee, strategie
Concetti, idee, strategieCache control tramite persisted queries

Cache control tramite persisted queries

GraphQL funziona generalmente tramite POST, eseguendo tutte le queries contro un unico endpoint e passando i parametri nel corpo della richiesta. L'URL di quell'unico endpoint produrrà risposte differenti, il che significa che non può essere memorizzato nella cache (almeno, non usando l'URL come identificatore).

Quindi il modo standard per supportare la cache in GraphQL è a livello del layer client, tramite il client Apollo e librerie simili, che memorizzano nella cache gli oggetti restituiti in modo indipendente l'uno dall'altro, identificandoli tramite il loro ID globale univoco.

(Al contrario, quando si memorizza nella cache lato server, normalmente usiamo l'URL come identificatore e memorizziamo nella cache i dati di tutte le entità della risposta tutti insieme.)

Ma questa soluzione presenta diversi svantaggi:

  • L'applicazione deve eseguire più JavaScript lato client. Accedere al sito web da un telefono cellulare di fascia bassa comporterà una perdita di prestazioni
  • L'applicazione diventa più complessa, con più parti mobili, poiché ora dobbiamo anche preoccuparci di implementare il layer di cache
  • Non tutti comprendono JavaScript (es.: il sito web potrebbe essere scritto in PHP), ma ora gestire JS diventa anche una responsabilità

Una soluzione molto migliore consiste nell'utilizzare la cache HTTP. Vediamo le condizioni preliminari necessarie affinché ciò funzioni.

Accedere a GraphQL tramite GET

Utilizzare la cache HTTP significa che memorizzeremo nella cache la risposta GraphQL usando l'URL come identificatore. Questo ha 2 implicazioni:

  1. Dobbiamo accedere al single endpoint di GraphQL tramite GET
  2. Dobbiamo passare la query e le variabili come parametri d'URL

Quindi, se il single endpoint è /graphql, l'operazione GET può essere eseguita contro l'URL /graphql?query=...&variables=....

Questo si applica al recupero dei dati dal server (tramite l'operazione query). Per modificare i dati (tramite l'operazione mutation), dobbiamo comunque usare POST. Non c'è alcun problema qui, poiché le mutations vengono sempre eseguite da capo; non possiamo memorizzare nella cache i risultati di una mutation, quindi non useremmo comunque la cache HTTP con essa.

Questo approccio funziona (ed è persino suggerito sul sito ufficiale), ma ci sono alcune considerazioni a cui dobbiamo prestare attenzione.

Codificare le queries GraphQL tramite un parametro d'URL

Una query GraphQL si estende normalmente su più righe. Ad esempio:

{
  posts {
    id
    title
  }
}

Tuttavia, non possiamo inserire questa stringa multilinea direttamente nel parametro d'URL.

La soluzione è codificarla. Ad esempio, il client GraphiQL codificherà la query qui sopra in questo modo:

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

Va bene, questo funziona. Ma non è molto leggibile, vero? Chi può capire quella query?

Una delle virtù di GraphQL è che le sue queries sono molto facili da comprendere. Con un po' di pratica, una volta vista la query, la capiamo immediatamente. Ma una volta codificata, tutto questo svanisce, e solo le macchine possono comprenderla; l'essere umano è escluso dall'equazione.

Un'altra soluzione potrebbe consistere nel sostituire tutti gli a capo della query con uno spazio, il che funziona perché gli a capo non aggiungono alcun significato semantico alla query. Così, la query qui sopra può essere rappresentata come:

?query={ posts { id title } }

Questo funziona bene per le queries semplici. Ma se hai una query davvero lunga, con molte { } aperte e chiuse, e l'aggiunta di argomenti dei campi e direttive, diventa sempre più difficile da comprendere.

Ad esempio, questa query:

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

Diventerebbe questa query su una sola riga:

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

Ancora una volta, l'esecuzione della query funzionerà, ma non sapremo cosa stiamo eseguendo.

E se la query contiene anche dei fragments, allora dimenticatelo del tutto, non c'è modo di averne una visione d'insieme.

Le persisted queries vengono in soccorso

Se passare la query nell'URL non è soddisfacente, quale altra opzione abbiamo? Beh, non passare la query nell'URL!

Questo è l'approccio chiamato «persisted query»: memorizziamo la query sul server e usiamo un identificatore (come un ID numerico, o una stringa univoca prodotta applicando un algoritmo di hashing con la query in input) per recuperarla. Infine, passiamo questo identificatore come parametro d'URL, al posto della query.

Ad esempio, la query potrebbe essere identificata dall'ID 2908 (o un hash come "50ac3e81"), e poi eseguiamo l'operazione GET contro l'URL /graphql?id=2908. Il server GraphQL recupererà quindi la query corrispondente a questo ID, la eseguirà e restituirà i risultati.

Gato GraphQL rende tutto questo ancora più semplice: una persisted query è implementata come un tipo di contenuto personalizzato, quindi possiamo crearne una e pubblicarla come un qualsiasi articolo normale, e lo slug che scegliamo (che per impostazione predefinita si basa sul titolo che inseriamo) diventerà il suo identificatore. Le persisted queries rendono banale l'implementazione della cache HTTP.

Calcolo del valore max-age

La cache HTTP funziona inviando l'header Cache-Control nella risposta, con un valore max-age che indica la quantità di tempo per cui la risposta deve essere memorizzata nella cache, oppure no-store per indicare di non memorizzarla nella cache.

Come farà il server GraphQL a calcolare il valore max-age per la query, considerando che campi diversi possono avere valori max-age diversi?

La risposta è: ottenere il valore max-age per tutti i campi richiesti nella query e determinare qual è il più basso. Quello sarà il max-age della risposta.

Ad esempio, supponiamo di avere un'entità di tipo User. Seguendo il comportamento assegnato a questa entità, possiamo definire per quanto tempo il campo corrispondente può essere memorizzato nella cache:

🛠 Il suo ID non cambierà mai ⇒ Diamo al campo id un max-age di 1 anno

🛠 Il suo URL verrà aggiornato molto raramente (semmai) ⇒ Diamo al campo url un max-age di 1 giorno

🛠 Il nome della persona può cambiare di tanto in tanto (es.: per aggiungere uno stato, o per dire «Milton (indossa una maschera)») ⇒ Diamo al campo name un max-age di 1 ora

🛠 Il karma dell'utente sul sito può cambiare in qualsiasi momento (es.: dopo che qualcuno ha votato positivamente il suo commento) ⇒ Diamo al campo karma un max-age di 1 minuto

🛠 Se interroghiamo i dati dell'utente connesso, la risposta non può essere memorizzata nella cache affatto (indipendentemente dal campo che stiamo recuperando) ⇒ Il max-age deve essere no-store

Di conseguenza, la risposta alle seguenti queries GraphQL avrà i seguenti valori max-age (per questo esempio, ignoriamo il max-age per il campo Root.users, ma in pratica verrà preso in considerazione anche quello):

QueryValore max-age
{
  users {
    id
  }
}
1 anno
{
  users {
    id
    url
  }
}
1 giorno
{
  users {
    id
    url
    name
  }
}
1 ora
{
  users {
    id
    url
    name
    karma
  }
}
1 minuto
{
  me {
    id
    url
    name
    karma
  }
}
no-store (non memorizzare nella cache)

Creazione della Cache Control List

Una volta che abbiamo identificato il max-age per ciascun campo, inseriamo queste informazioni tramite una Cache Control List:

Definizione di una policy di cache control

Gato GraphQL calcolerà quindi automaticamente il valore max-age della risposta e lo restituirà come header HTTP Cache-Control.