Deprecacja GraphQL bez numeru wersji
5 min czytania
API REST może publikować /v2/ obok /v1/ i pozwolić wywołującym migrować we własnym tempie.
GraphQL ma jeden schemat na jednym endpoincie, i każdy klient, aplikacja mobilna na zeszłorocznym
buildzie i wewnętrzny dashboard wdrożony dziś rano, odpytuje ten sam graf. Nie ma URL-a do
zforkowania. Deprecjonowanie pola oznacza oznaczenie go jako zdeprecjonowanego na miejscu, w
schemacie, od którego wszyscy już zależą, co czyni dyscyplinę inną niż w REST, mimo że leżący u
podstaw problem, mówienie wywołującym, że coś zniknie, jest tym samym, który ogólnie ujmuje
deprecacja API.
Jak GraphQL oznacza pole jako zdeprecjonowane, skoro nie ma wersji do podniesienia?
Dyrektywą @deprecated, zastosowaną bezpośrednio do pola:
type Product {
price: Float @deprecated(reason: "Use priceV2 for multi-currency support.")
priceV2: Money
}
Pole pozostaje możliwe do zapytania. Nie znika, nie zwraca 404, nie zmienia zachowania; niesie tylko notatkę czytelną maszynowo, którą większość narzędzi GraphQL, GraphiQL, Apollo Studio, lintery schematu, pokaże każdemu, kto przegląda schemat lub pisze zapytanie przeciwko niemu. To cały mechanizm. Nie ma osobnego endpointu deprecacji, żadnego nagłówka, żadnego towarzyszącego dokumentu wymaganego przez specyfikację, co jest zarówno atutem, jak i pułapką: dyrektywę łatwo dodać i łatwo zignorować, bo nic nie zmusza klienta, żeby na nią spojrzał.
Czy ktoś w ogóle widzi powód deprecacji?
Tylko ci, którzy używają schematu bezpośrednio, przez introspekcję lub edytor świadomy schematu, a
to mniejsza publiczność niż zwykli czytelnicy changeloga API. Aplikacja mobilna zbudowana na
zapytaniu sprzed sześciu miesięcy ma to zapytanie już wypieczone w swoim binarce; będzie dalej
pytać o price i dalej dostawać odpowiedź, zdeprecjonowaną albo nie, dopóki ktoś nie przebuduje
aplikacji z nowym polem i nie wyda aktualizacji. Dyrektywa mówi deweloperce piszącej nowy kod, żeby
nie używała starego pola. Nic nie robi dla klienta już wydanego i działającego.
| Mechanizm | Do kogo dociera |
|---|---|
Dyrektywa @deprecated | Deweloperki przeglądające schemat lub piszące nowe zapytania |
| Błędy CI lintera schematu | Zespół właściciel kodu klienckiego, jeśli taki uruchamia |
| Wpis w changelogu | Ktokolwiek go przeczyta, w tym zespół kliencki bez lintera |
| Nic (pole po prostu działa) | Już zbudowany klient używający starego pola |
Czy zdeprecjonowane pole powinno mimo to dostać wpis w changelogu?
Tak, i robi więcej niż sama dyrektywa, bo changelog dociera do ludzi, do których dyrektywa nie dociera: zespół partnerski, który konsumuje graf bez przeglądania jego schematu, klient zbudowany na zcache’owanej kopii schematu sprzed miesięcy, każdy, kto zauważyłby to tylko czytając prozę. Changelog API ogólnie ujmuje, co wpis jest winien wywołującemu; wpis GraphQL jest winien jedną rzecz, której REST rzadko musi wyjaśniać wprost, bo wywołujący REST wywnioskowują ją z numeru wersji: czy stare pole nadal działa dziś, nadal działa z ostrzeżeniem, czy faktycznie przestało zwracać dane. Sama dyrektywa nie odpowiada na nic z tego dla czytelniczki, która nigdy nie otworzyła schematu.
Kiedy faktycznie bezpiecznie jest usunąć pole ze schematu?
Dopiero gdy logi zapytań pokazują, że nikt już o nie nie pyta, co jest pytaniem o użycie, nie o
kalendarz. Pole może nosić @deprecated przez rok i wciąż być nośne dla jednego klienta, który
nigdy nie został przebudowany; usunięcie go w ustalonym harmonogramie, jak często robi to
REST-owy Sunset, łamie tego klienta bez żadnego ostrzeżenia, na które mógłby zareagować, bo
GraphQL nie daje mu niczego, na czym mógłby się oprzeć, poza dyrektywą, której nigdy nie
przeczytał. Logujcie użycie na poziomie pola, zanim zobowiążecie się do daty usunięcia, i
traktujcie każdą niezerową liczbę zapytań jako wstrzymanie, nie odliczanie.
Czy dodanie pola niesie takie samo ryzyko jak w API REST?
Mniejsze, dla nowego pola, bo klient GraphQL dostaje tylko pola, o które jawnie prosi. Dodanie
priceV2 obok price nie może złamać istniejącego zapytania w sposób, w jaki dodanie pola do
odpowiedzi JSON REST może złamać ścisły deserializator, bo nic nie zmusza klienta do zażądania
nowego pola. Dodanie wartości do istniejącego enuma to wyjątek, o którym warto wspomnieć w tym
samym tchu: klient, który przełącza się na każdej wartości enuma wyczerpująco, do czego zachęcają
silnie typowane języki, łamie się w momencie pojawienia się nowej wartości, niezależnie od tego,
czy jakiekolwiek zapytanie o nią prosiło. To bezpieczeństwo dotyczy tylko pól i elementów unii, w
które klient wchodzi dobrowolnie; nie dotyczy zamkniętego zbioru, który kod klienta wylicza
ręcznie.
Czego potrzebuje wpis changeloga GraphQL, czego nie potrzebuje wpis REST?
Kształtu zapytania, nie tylko nazwy pola, bo “pole price jest zdeprecjonowane” brakuje właśnie
tego kawałka, którego wywołujący naprawdę potrzebuje: które typy i które zapytania go dotykają.
Przydatny wpis nazywa typ, pole, pole zastępcze i, jeśli można to wygenerować, rzeczywiste
zapytania w produkcji nadal proszące o starą formę. Ten ostatni element, powiązanie powiadomienia
o deprecacji z rzeczywistym użyciem, to coś, co wywołujący REST dostają za darmo z logów serwera
na URL-u, a wywołujący GraphQL nie, bo każde zapytanie trafia w ten sam endpoint bez względu na
to, o co pyta.
Czy coś innego niż pole może nieść dyrektywę @deprecated?
Wartości enuma, tą samą dyrektywą, ale na definicji samej wartości, nie pola:
enum ShippingMethod {
STANDARD
EXPRESS
OVERNIGHT @deprecated(reason: "Use EXPRESS with priority: true instead.")
}
Specyfikacja definiuje @deprecated dokładnie dla dwóch miejsc, definicji pola albo wartości
enuma, i niczego więcej w stabilnym wydaniu; deprecacja na poziomie argumentu czy pola wejściowego
istnieje tylko w późniejszym języku roboczym, nie w tym, co wdraża dziś większość serwerów.
Wartość enuma oznaczona w ten sposób pozostaje legalną wartością, którą serwer wciąż może zwrócić
albo przyjąć, ta sama nie-łamiąca obietnica, jaką daje zdeprecjonowane pole, co czyni ją bezpieczną
do wydania, zanim wartość zostanie naprawdę usunięta.
FAQ
Czy GraphQL wspiera coś w rodzaju nagłówka Sunset dla całego endpointu?
Nie, bo zwykle jest tylko jeden endpoint. Harmonogram deprecacji żyje na poziomie pola, w tekście
powodu dyrektywy @deprecated i w jakimkolwiek changelogu czy przewodniku migracji, który zespół
opublikuje obok, nie w nagłówku odpowiedzi, który klient mógłby odczytać programowo.
Czy zdeprecjonowane pole może zostać usunięte, a potem dodane ponownie z innym typem?
Tylko jako nowa nazwa pola. Ponowne wprowadzenie tej samej nazwy pola ze zmienionym typem to
dokładnie ta zmiana łamiąca kompatybilność, której cykl deprecacji istnieje, by uniknąć; dajcie
zastępstwu własną nazwę, tak jak robi priceV2, i pozwólcie staremu całkowicie wygasnąć, zanim
nazwa będzie wolna do ponownego użycia.
Czy tekst powodu @deprecated powinien linkować do wpisu w changelogu?
Tak, gdy narzędzia schematu to wspierają. Pole powodu przyjmuje zwykły string, a URL wewnątrz
tego stringa to najkrótsza droga od deweloperki wpatrującej się w wynik introspekcji do
pełniejszego wyjaśnienia, jakie może dać wpis changeloga.
Czy zmiana schematu GraphQL jest kiedykolwiek wstecznie kompatybilna w sposób, w jaki REST nie jest? Addytywne zmiany pól, tak, z powyższego powodu: klienci dostają tylko to, o co proszą. Nowe wartości enuma są wyjątkiem, bo klient wyliczający zamknięty zbiór może złamać się na wartości, której nie oczekiwał. Usunięcia i zmiany typów są dokładnie tak samo łamiące, jak ich REST-owe odpowiedniki.
Twierdzenia techniczne w tym artykule nie zostały niezależnie zweryfikowane. Jeśli coś się nie zgadza, daj nam znać, a poprawimy to.