GraphQL-Abkündigung ohne Versionsnummer
5 Min. Lesezeit
Eine REST-API kann /v2/ neben /v1/ ausliefern und Aufrufer in ihrem eigenen Tempo wechseln
lassen. GraphQL hat ein Schema an einem Endpunkt, und jeder Client, die Mobile-App auf letztjährigem
Build und das interne Dashboard von heute Morgen, fragt denselben Graphen ab. Es gibt keine URL zum
Forken. Ein Feld abzukündigen bedeutet, es an Ort und Stelle als abgekündigt zu markieren, in einem
Schema, von dem schon alle abhängen, was die Disziplin anders macht als bei REST, obwohl das
zugrunde liegende Problem, Aufrufern zu sagen, dass etwas verschwindet, dasselbe ist, das
API-Abkündigung allgemein behandelt.
Wie markiert GraphQL ein Feld als abgekündigt, wenn es keine Version zum Erhöhen gibt?
Mit der @deprecated-Direktive, angewendet direkt auf das Feld:
type Product {
price: Float @deprecated(reason: "Use priceV2 for multi-currency support.")
priceV2: Money
}
Das Feld bleibt abfragbar. Es verschwindet nicht, gibt keinen 404 zurück, ändert das Verhalten nicht; es trägt nur eine maschinenlesbare Notiz, die die meisten GraphQL-Tools, GraphiQL, Apollo Studio, Schema-Linter, jedem zeigen, der das Schema durchsucht oder eine Query dagegen schreibt. Das ist der gesamte Mechanismus. Es gibt keinen separaten Abkündigungs-Endpunkt, keinen Header, kein vom Spec verlangtes Begleitdokument, was sowohl den Reiz als auch die Falle ausmacht: Die Direktive ist leicht hinzuzufügen und leicht zu ignorieren, weil nichts einen Client zwingt, hinzuschauen.
Sieht überhaupt jemand den Abkündigungsgrund?
Nur Leute, die das Schema direkt nutzen, durch Introspektion oder einen schemabewussten Editor, und
das ist ein kleineres Publikum als die üblichen Leser eines API-Changelogs. Eine Mobile-App, die vor
sechs Monaten gegen eine Query gebaut wurde, hat diese Query schon in ihr Binary eingebacken; sie
wird weiter nach price fragen und weiter eine Antwort bekommen, abgekündigt oder nicht, bis jemand
die App mit dem neuen Feld neu baut und ein Update ausliefert. Die Direktive sagt einer
Entwicklerin, die neuen Code schreibt, das alte Feld nicht zu nutzen. Für den bereits ausgelieferten
und laufenden Client tut sie nichts.
| Mechanismus | Wen er erreicht |
|---|---|
@deprecated-Direktive | Entwicklerinnen, die das Schema durchsuchen oder neue Queries schreiben |
| CI-Fehler durch Schema-Linter | Das Team, dem die Client-Codebase gehört, sofern es einen betreibt |
| Ein Changelog-Eintrag | Wer auch immer ihn liest, auch ein Client-Team ohne Linter |
| Nichts (Feld funktioniert einfach) | Ein bereits gebauter Client, der das alte Feld nutzt |
Sollte ein abgekündigtes Feld trotzdem einen Changelog-Eintrag bekommen?
Ja, und der leistet mehr als die Direktive allein, weil ein Changelog Leute erreicht, die die Direktive nicht erreicht: ein Partnerteam, das den Graphen konsumiert, ohne sein Schema zu durchsuchen, ein Client, der gegen eine monatealte gecachte Kopie des Schemas gebaut wurde, jede, die es nur durch Lesen von Prosa bemerken würde. API-Changelog behandelt allgemein, was ein Eintrag einem Aufrufer schuldet; ein GraphQL-Eintrag schuldet eine Sache, die REST selten ausbuchstabieren muss, weil REST-Aufrufer sie aus der Versionsnummer ableiten: ob das alte Feld heute noch funktioniert, mit Warnung noch funktioniert, oder tatsächlich aufgehört hat, Daten zurückzugeben. Die Direktive allein beantwortet davon nichts für eine Leserin, die das Schema nie geöffnet hat.
Wann ist ein Feld tatsächlich sicher aus dem Schema zu entfernen?
Erst wenn Query-Logs zeigen, dass niemand mehr danach fragt, was eine Nutzungsfrage ist, keine
Kalenderfrage. Ein Feld kann ein Jahr lang @deprecated tragen und trotzdem tragend sein für einen
Client, der nie neu gebaut wurde; es auf einem festen Zeitplan zu entfernen, so wie es ein
REST-Sunset-Header oft tut, bricht diesen Client ohne Warnung, auf die er reagieren kann, weil
GraphQL ihm nichts gibt, worauf er reagieren könnte, außer der Direktive, die er nie gelesen hat.
Loggt die feldweise Nutzung, bevor ihr euch auf ein Entfernungsdatum festlegt, und behandelt eine
Query-Zahl über null als Bremse, nicht als Countdown.
Trägt das Hinzufügen eines Felds dasselbe Risiko wie bei einer REST-API?
Weniger, für ein neues Feld, weil ein GraphQL-Client nur die Felder erhält, um die er explizit
bittet. priceV2 neben price hinzuzufügen kann eine bestehende Query nicht auf die Weise
brechen, wie das Hinzufügen eines Felds zu einer REST-JSON-Antwort einen strikten Deserializer
brechen kann, weil nichts den Client zwingt, das neue Feld anzufragen. Einen Wert zu einem
bestehenden Enum hinzuzufügen ist die Ausnahme, die es wert ist, im selben Atemzug zu nennen: ein
Client, der über jeden Enum-Wert erschöpfend switcht, was streng typisierte Sprachen fördern, bricht
in dem Moment, in dem ein neuer Wert auftaucht, egal ob irgendeine Query danach gefragt hat. Die
Sicherheit gilt nur für Felder und Union-Mitglieder, die ein Client bewusst anfragt; sie gilt nicht
für eine geschlossene Menge, die der Code eines Clients von Hand aufzählt.
Was braucht ein GraphQL-Changelog-Eintrag, das ein REST-Eintrag nicht braucht?
Die Query-Form, nicht nur den Feldnamen, weil „das Feld price ist abgekündigt” genau das fehlende
Stück ist, das eine Aufruferin tatsächlich braucht: welche Typen und welche Queries es berühren.
Ein nützlicher Eintrag nennt den Typ, das Feld, das Ersatzfeld und, wenn ihr es generieren könnt,
die tatsächlichen Queries in Produktion, die noch die alte Form anfragen. Dieses letzte Stück, die
Abkündigungsmeldung mit echter Nutzung zu verknüpfen, ist das, was REST-Aufrufer aus Server-Logs zu
einer URL kostenlos bekommen und GraphQL-Aufrufer nicht, weil jede Query denselben Endpunkt trifft,
egal was sie anfragt.
Kann außer einem Feld noch etwas anderes die @deprecated-Direktive tragen?
Enum-Werte, mit derselben Direktive an der Definition des Werts selbst statt am Feld:
enum ShippingMethod {
STANDARD
EXPRESS
OVERNIGHT @deprecated(reason: "Use EXPRESS with priority: true instead.")
}
Die Spec definiert @deprecated für genau zwei Stellen, eine Feld-Definition oder einen Enum-Wert,
und nichts sonst im Stand des stabilen Releases; Deprecation auf Argument- oder Input-Feld-Ebene
existiert nur in späterer Draft-Sprache, nicht in dem, was die meisten Server heute implementieren.
Ein so markierter Enum-Wert bleibt ein gültiger Wert, den ein Server weiterhin zurückgeben oder
akzeptieren kann, dasselbe nicht-brechende Versprechen wie bei einem abgekündigten Feld, was ihn
sicher macht, bevor der Wert wirklich entfernt wird.
FAQ
Unterstützt GraphQL irgendetwas wie einen Sunset-Header für einen ganzen Endpunkt?
Nein, weil es meist nur einen Endpunkt gibt. Das Timing der Abkündigung lebt auf Feldebene, im
Grundtext der @deprecated-Direktive und in was auch immer für ein Changelog oder Migrationsguide
ein Team daneben veröffentlicht, nicht in einem Response-Header, den ein Client programmatisch
lesen kann.
Kann ein abgekündigtes Feld später mit einem anderen Typ neu hinzugefügt werden?
Nur als neuer Feldname. Denselben Feldnamen mit geändertem Typ wieder einzuführen ist genau der
Breaking Change, den der Abkündigungszyklus vermeiden soll; gebt dem Ersatz einen eigenen Namen, so
wie priceV2 es tut, und lasst den alten vollständig ausklingen, bevor der Name wieder frei wird.
Sollte der @deprecated-Grundtext auf den Changelog-Eintrag verlinken?
Ja, wenn das Schema-Tooling das unterstützt. Das Grund-Feld akzeptiert einen einfachen String, und
eine URL darin ist der kürzeste Weg von einer Entwicklerin, die auf Introspektions-Output starrt,
zur ausführlicheren Erklärung, die ein Changelog-Eintrag geben kann.
Ist eine GraphQL-Schema-Änderung je abwärtskompatibel auf eine Art, die REST nicht ist? Additive Feldänderungen ja, aus dem oben genannten Grund: Clients bekommen nur, wonach sie fragen. Neue Enum-Werte sind die Ausnahme, weil ein Client, der eine geschlossene Menge aufzählt, an einem Wert brechen kann, den er nicht erwartet hat. Entfernungen und Typänderungen sind genau so brechend wie ihre REST-Äquivalente.
Die technischen Aussagen in diesem Artikel wurden nicht unabhängig geprüft. Wenn etwas nicht stimmt, sagen Sie es uns, und wir korrigieren es.