Cache control via utrwalone queries
GraphQL zazwyczaj działa przez POST, wykonując wszystkie queries przeciwko jednemu endpointowi i przekazując parametry w ciele żądania. URL tego jednego endpointu będzie generować różne odpowiedzi, co oznacza, że nie może być przechowywany w pamięci podręcznej (przynajmniej nie przy użyciu URL jako identyfikatora).
Dlatego standardowym sposobem obsługi cache w GraphQL jest warstwa klienta, za pomocą klienta Apollo i podobnych bibliotek, które przechowują zwrócone obiekty w pamięci podręcznej niezależnie od siebie, identyfikując je po ich unikalnym globalnym ID.
(W odróżnieniu od tego, gdy przechowujemy dane w pamięci podręcznej po stronie serwera, zazwyczaj używamy URL jako identyfikatora i przechowujemy dane wszystkich encji z odpowiedzi łącznie.)
Ale to rozwiązanie ma kilka wad:
- Aplikacja musi uruchamiać więcej JavaScriptu po stronie klienta. Dostęp do strony przez tani smartfon spowoduje pogorszenie wydajności
- Aplikacja staje się bardziej złożona i ma więcej ruchomych części, ponieważ teraz musimy też martwić się o implementację warstwy cache
- Nie wszyscy rozumieją JavaScript (np. strona może być napisana w PHP), ale teraz zajmowanie się JS staje się dodatkową odpowiedzialnością
Znacznie lepszym rozwiązaniem jest użycie cache HTTP. Zobaczmy, jakie warunki wstępne są potrzebne, aby to zadziałało.
Dostęp do GraphQL przez GET
Używanie cache HTTP oznacza, że będziemy przechowywać odpowiedź GraphQL w pamięci podręcznej używając URL jako identyfikatora. Ma to 2 implikacje:
- Musimy uzyskiwać dostęp do jedynego endpointu GraphQL przez
GET - Musimy przekazywać query i zmienne jako parametry URL
Zatem jeśli jedyny endpoint to /graphql, operacja GET może być wykonana pod URL /graphql?query=...&variables=....
Dotyczy to pobierania danych z serwera (za pomocą operacji query). Do modyfikowania danych (za pomocą operacji mutation) nadal musimy używać POST. Nie ma tu problemu, ponieważ mutations są zawsze wykonywane od nowa; nie możemy przechowywać wyników mutation w pamięci podręcznej, więc i tak nie używalibyśmy z nią cache HTTP.
To podejście działa (i jest nawet sugerowane na oficjalnej stronie), ale są pewne kwestie, na które musimy zwracać uwagę.
Kodowanie queries GraphQL przez parametr URL
Query GraphQL zazwyczaj obejmuje wiele linii. Na przykład:
{
posts {
id
title
}
}Nie możemy jednak wpisać tego wieloliniowego ciągu bezpośrednio w parametrze URL.
Rozwiązaniem jest zakodowanie go. Na przykład klient GraphiQL zakoduje powyższą query w ten sposób:
%7B%0A%20%20posts%20%7B%0A%20%20%20%20id%0A%20%20%20%20title%0A%20%20%7D%0A%7D
Dobrze, to działa. Ale nie wygląda zbyt dobrze, prawda? Kto jest w stanie zrozumieć tę query?
Jedną z zalet GraphQL jest to, że jego queries są bardzo łatwe do zrozumienia. Po pewnej praktyce, gdy tylko zobaczymy query, od razu ją rozumiemy. Ale po zakodowaniu wszystko to znika i tylko maszyny mogą ją pojąć; człowiek wypada z równania.
Innym rozwiązaniem byłoby zastąpienie wszystkich znaków nowej linii w query spacją, co działa ponieważ znaki nowej linii nie dodają żadnego znaczenia semantycznego do query. Query powyżej można wtedy przedstawić jako:
?query={ posts { id title } }
Działa to dobrze w przypadku prostych queries. Ale jeśli masz naprawdę długą query, otwierającą i zamykającą wiele { } oraz dodającą argumenty pól i dyrektywy, staje się to coraz trudniejsze do zrozumienia.
Na przykład ta 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
}
}
}Stałaby się tą jednoliniową 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 } } }
Ponownie, wykonanie query zadziała, ale nie będziemy wiedzieć, co wykonujemy.
A jeśli query zawiera też fragmenty, to absolutnie zapomnij — nie ma możliwości, żeby to zrozumieć.
Utrwalone queries z pomocą
Jeśli przekazywanie query w URL nie jest zadowalające, jaką mamy inną opcję? Otóż, nie przekazywać query w URL!
To jest podejście zwane "persisted query": przechowujemy query na serwerze i używamy identyfikatora (takiego jak numeryczne ID lub unikalny ciąg znaków uzyskany przez zastosowanie algorytmu haszowania z query jako danymi wejściowymi) do jej pobrania. Na koniec przekazujemy ten identyfikator jako parametr URL zamiast query.
Na przykład query może być identyfikowana z ID 2908 (lub hashem takim jak "50ac3e81"), a następnie wykonujemy operację GET pod URL /graphql?id=2908. Serwer GraphQL pobierze wtedy query odpowiadającą temu ID, wykona ją i zwróci wyniki.
Gato GraphQL sprawia, że jest to jeszcze prostsze: utrwalona query jest implementowana jako niestandardowy typ wpisu, więc możemy ją utworzyć i opublikować jak każdy zwykły wpis, a wybrany przez nas slug (który domyślnie oparty jest na wprowadzonym tytule) stanie się jej identyfikatorem. Utrwalone queries sprawiają, że implementacja cache HTTP jest trywialna.
Obliczanie wartości max-age
Cache HTTP działa poprzez wysyłanie nagłówka Cache-Control w odpowiedzi, z wartością max-age wskazującą czas, przez jaki odpowiedź ma być przechowywana w pamięci podręcznej, lub no-store wskazującą, że nie należy jej cachować.
Jak serwer GraphQL obliczy wartość max-age dla query, biorąc pod uwagę, że różne pola mogą mieć różne wartości max-age?
Odpowiedź brzmi: Pobierz wartość max-age dla wszystkich pól żądanych w query i sprawdź, która jest najniższa. To będzie max-age odpowiedzi.
Na przykład, powiedzmy, że mamy encję typu User. Zgodnie z zachowaniem przypisanym tej encji, możemy określić, jak długo odpowiednie pole może być przechowywane w pamięci podręcznej:
🛠 Jego ID nigdy się nie zmieni ⇒ Nadajemy polu id max-age 1 roku
🛠 Jego URL będzie aktualizowany bardzo rzadko (jeśli w ogóle) ⇒ Nadajemy polu url max-age 1 dnia
🛠 Imię osoby może się zmieniać od czasu do czasu (np. aby dodać status lub powiedzieć "Milton (nosi maskę)") ⇒ Nadajemy polu name max-age 1 godziny
🛠 Karma użytkownika na stronie może zmieniać się w każdej chwili (np. po tym jak ktoś polubi jego komentarz) ⇒ Nadajemy polu karma max-age 1 minuty
🛠 Jeśli pobieramy dane zalogowanego użytkownika, odpowiedź nie może być w ogóle cachowana (niezależnie od tego, które pole pobieramy) ⇒ max-age musi wynosić no-store
W rezultacie odpowiedź na następujące queries GraphQL będzie miała następujące wartości max-age (w tym przykładzie pomijamy max-age dla pola Root.users, ale w praktyce będzie ono również brane pod uwagę):
| Query | Wartość max-age |
|---|---|
| 1 rok |
| 1 dzień |
| 1 godzina |
| 1 minuta |
| no-store (nie cachować) |
Tworzenie Cache Control List
Po zidentyfikowaniu max-age dla każdego pola, wprowadzamy te informacje za pomocą Cache Control List:

Gato GraphQL automatycznie obliczy wtedy wartość max-age odpowiedzi i odeśle ją jako nagłówek HTTP Cache-Control.