Deprecierea GraphQL fără un număr de versiune
6 min de citit
Un API REST poate publica /v2/ alături de /v1/ și poate lăsa apelanții să migreze în ritmul
lor. GraphQL are o schemă la un endpoint, și fiecare client, aplicația mobilă pe build-ul de anul
trecut și dashboard-ul intern lansat azi-dimineață, interoghează același graf. Nu există un URL de
bifurcat. Deprecierea unui câmp înseamnă marcarea lui ca depreciat pe loc, într-o schemă de care
toată lumea deja depinde, ceea ce face disciplina diferită față de REST, chiar dacă problema de
fond, să le spui apelanților că ceva va dispărea, e aceeași pe care o acoperă deprecierea
API în general.
Cum marchează GraphQL un câmp ca depreciat, dacă nu există versiune de incrementat?
Cu directiva @deprecated, aplicată direct pe câmp:
type Product {
price: Float @deprecated(reason: "Use priceV2 for multi-currency support.")
priceV2: Money
}
Câmpul rămâne interogabil. Nu dispare, nu returnează un 404, nu își schimbă comportamentul; poartă doar o notă lizibilă de mașini pe care majoritatea uneltelor GraphQL, GraphiQL, Apollo Studio, linter-ele de schemă, o vor arăta oricui răsfoiește schema sau scrie o interogare împotriva ei. Ăsta e întregul mecanism. Nu există un endpoint de depreciere separat, niciun header, niciun document însoțitor cerut de specificație, ceea ce e atât atracția, cât și capcana: directiva e ușor de adăugat și ușor de ignorat, pentru că nimic nu obligă un client să se uite la ea.
Vede cineva de fapt motivul deprecierii?
Doar cei care folosesc schema direct, prin introspecție sau un editor conștient de schemă, și
acesta e un public mai mic decât cititorii obișnuiți ai unui changelog de API. O aplicație mobilă
construită împotriva unei interogări acum șase luni are deja acea interogare coaptă în binarul ei;
va continua să ceară price și va continua să primească un răspuns, depreciat sau nu, până
cineva reconstruiește aplicația cu noul câmp și lansează o actualizare. Directiva îi spune unei
dezvoltatoare care scrie cod nou să nu folosească câmpul vechi. Nu face nimic pentru clientul deja
lansat și în funcțiune.
| Mecanism | Pe cine ajunge |
|---|---|
Directiva @deprecated | Dezvoltatoare care răsfoiesc schema sau scriu interogări noi |
| Eșecuri CI ale linter-ului de schemă | Echipa care deține codul client, dacă rulează unul |
| O intrare de changelog | Oricine o citește, inclusiv o echipă client fără linter |
| Nimic (câmpul funcționează pur și simplu) | Un client deja construit care folosește câmpul vechi |
Ar trebui un câmp depreciat să primească totuși o intrare de changelog?
Da, și face mai multă treabă decât directiva singură, pentru că un changelog ajunge la oameni pe care directiva nu îi poate ajunge: o echipă parteneră care consumă graful fără să-i răsfoiască schema, un client construit împotriva unei copii cache-uite a schemei de acum luni, oricine ar observa doar citind proză. Changelog de API acoperă în general ce datorează o intrare unui apelant; o intrare GraphQL datorează un lucru pe care REST rareori trebuie să-l explice, pentru că apelanții REST îl deduc din numărul de versiune: dacă vechiul câmp mai funcționează azi, mai funcționează cu un avertisment, sau chiar a încetat să mai returneze date. Directiva singură nu răspunde la niciuna dintre astea pentru o cititoare care nu a deschis niciodată schema.
Când e de fapt sigur să elimini un câmp din schemă?
Doar când jurnalele de interogări arată că nimeni nu mai cere acel câmp, ceea ce e o întrebare
despre utilizare, nu despre calendar. Un câmp poate purta @deprecated un an întreg și tot să fie
esențial pentru un client care nu a fost niciodată reconstruit; eliminarea lui pe un calendar fix,
cum face adesea un Sunset REST, strică acel client fără niciun avertisment pe care să poată
acționa, pentru că GraphQL nu-i dă nimic pe care să acționeze în afară de directiva pe care n-a
citit-o niciodată. Înregistrați utilizarea la nivel de câmp înainte să vă angajați la o dată de
eliminare, și tratați orice contor de interogări diferit de zero ca o pauză, nu ca o numărătoare
inversă.
Adăugarea unui câmp poartă același risc ca într-un API REST?
Mai puțin, pentru un câmp nou, pentru că un client GraphQL primește doar câmpurile pe care le cere
explicit. Adăugarea priceV2 lângă price nu poate strica o interogare existentă în modul în care
adăugarea unui câmp la un răspuns JSON REST poate strica un deserializator strict, pentru că nimic
nu obligă clientul să ceară noul câmp. Adăugarea unei valori la un enum existent e excepția care
merită numită în aceeași respirație: un client care face switch exhaustiv pe fiecare valoare de
enum, ceea ce limbajele cu tipare strictă încurajează, se strică în momentul în care apare o
valoare nouă, indiferent dacă vreo interogare a cerut-o. Siguranța ține doar pentru câmpuri și
membri de union la care un client aderă opțional; nu ține pentru o mulțime închisă pe care codul
unui client o enumeră de mână.
Ce are nevoie o intrare de changelog GraphQL de care nu are nevoie una REST?
Forma interogării, nu doar numele câmpului, pentru că “câmpul price e depreciat” îi lipsește
exact piesa de care are nevoie de fapt un apelant: ce tipuri și ce interogări îl ating. O intrare
utilă numește tipul, câmpul, câmpul de înlocuire și, dacă puteți genera asta, interogările reale
din producție care încă cer forma veche. Acea ultimă piesă, legarea notificării de depreciere de
utilizarea reală, e ceea ce apelanții REST primesc gratis din jurnalele serverului pe un URL, iar
apelanții GraphQL nu, pentru că fiecare interogare lovește același endpoint indiferent ce cere.
Poate purta directiva @deprecated altceva în afară de un câmp?
Valorile de enum, folosind aceeași directivă chiar la definiția valorii, nu a câmpului:
enum ShippingMethod {
STANDARD
EXPRESS
OVERNIGHT @deprecated(reason: "Use EXPRESS with priority: true instead.")
}
Specificația definește @deprecated pentru exact două locuri, o definiție de câmp sau o valoare de
enum, și nimic altceva în versiunea stabilă; deprecierea la nivel de argument sau de câmp de input
există doar în limbajul de draft ulterior, nu în ce implementează majoritatea serverelor azi. O
valoare de enum marcată astfel rămâne o valoare legală pe care un server o poate încă returna sau
accepta, aceeași promisiune de neîntrerupere pe care o face un câmp depreciat, ceea ce e ce face
sigură lansarea ei înainte de a elimina valoarea de-a binelea.
FAQ
GraphQL suportă ceva de genul unui header Sunset pentru un întreg endpoint?
Nu, pentru că de obicei există un singur endpoint. Calendarul deprecierii trăiește la nivel de
câmp, în textul motivului directivei @deprecated și în orice changelog sau ghid de migrare pe
care o echipă îl publică alături, nu într-un header de răspuns pe care un client l-ar putea citi
programatic.
Poate fi eliminat un câmp depreciat și readăugat mai târziu cu un tip diferit?
Doar ca nume de câmp nou. Reintroducerea aceluiași nume de câmp cu un tip schimbat e exact
schimbarea care rupe compatibilitatea pe care ciclul de depreciere există să o evite; dați
înlocuitorului propriul nume, așa cum face priceV2, și lăsați-l pe cel vechi să se stingă complet
înainte ca numele să fie liber pentru reutilizare.
Ar trebui textul motivului @deprecated să lege către intrarea de changelog?
Da, când uneltele de schemă permit asta. Câmpul motiv acceptă un string simplu, iar un URL în
interiorul acelui string e cel mai scurt drum de la o dezvoltatoare care se holbează la output-ul
de introspecție la explicația mai completă pe care o poate da o intrare de changelog.
E vreodată o schimbare de schemă GraphQL compatibilă retroactiv într-un mod în care REST nu e? Schimbările aditive de câmpuri, da, din motivul de mai sus: clienții primesc doar ce cer. Valorile noi de enum sunt excepția, pentru că un client care enumeră o mulțime închisă se poate strica pe una la care nu se aștepta. Eliminările și schimbările de tip sunt exact la fel de rupătoare ca echivalentele lor REST.
Afirmațiile tehnice din acest articol nu au fost verificate independent. Dacă ceva nu e corect, spune-ne și vom corecta.