Die Anwendung so gestalten, dass sie mit verschiedenen GraphQL-Servern funktioniert
„Gegen Schnittstellen programmieren, nicht gegen Implementierungen" ist die Praxis, eine Funktionalität nicht direkt aufzurufen, sondern über einen Vertrag, der beschreibt, welche Eingaben erforderlich sind und welche Ausgabe erwartet wird, während die konkrete Implementierung verborgen bleibt. Diese Strategie hilft dabei, die Anwendung von einer bestimmten Implementierung, einem Anbieter oder einem Stack zu entkoppeln und einen Austausch zu ermöglichen, ohne den Anwendungscode ändern zu müssen.
Wir können diese Strategie auch mit GraphQL anwenden. GraphQL kann als Vermittler zwischen der Anwendung und dem Server fungieren und es uns ermöglichen, alle notwendigen Änderungen nur an den GraphQL-Queries vorzunehmen, während die Geschäftslogik unberührt bleibt.
Eine GraphQL-Query fungiert als Schnittstelle zwischen Client und Server. Beim Ausführen einer Query verarbeitet der GraphQL-Server sie und gibt die angeforderten Daten an den Client zurück. Woher kommen die Daten? Wie wurden sie abgerufen? Der Client weiß es nicht und interessiert sich auch nicht dafür.

Die Antwort auf die Query hat dieselbe Form wie die Query. Für diese GraphQL-Query:
{
post(by: { id: 1 }) {
id
title
}
}...lautet die Antwort:
{
"data": {
"post": {
"id": 1,
"title": "Hello world!"
}
}
}Bei derselben Query mit anderen Parametern werden die zurückgegebenen Daten unterschiedlich sein, aber die Form bleibt konstant. Das bedeutet, dass die Anwendung ihre Logik zum Lesen und Verarbeiten der Daten nicht ändern muss, solange sich die Query nicht ändert, und es spielt ebenfalls keine Rolle, welcher GraphQL-Server die Query ausführt.
Und so können wir einen GraphQL-Server nahtlos gegen einen anderen austauschen.
Queries hängen vom GraphQL-Schema ab
Der letzte Abschnitt ist nun etwas zu optimistisch, denn die GraphQL-Query muss sich möglicherweise je nach GraphQL-Server ändern. Genauer gesagt basiert die Query auf dem GraphQL-Schema, und wenn verschiedene Server unterschiedliche Schemas bereitstellen, wird auch die Query unterschiedlich sein.
Ein GraphQL-Server, der beispielsweise die Cursor Connections Specification verwendet, führt möglicherweise die folgende Query aus:
{
categories(first: 10000) {
edges {
node {
categoryId
description
id
name
slug
}
}
}
}Ein anderer Server, der WordPress-ähnliche Paginierung verwendet (wie Gato GraphQL), führt dieselbe Query folgendermaßen aus:
{
postCategories(pagination: { limit: 10000 }) {
id
description
globalID
name
slug
}
}Wir können die Unterschiede zwischen den beiden Queries erkennen:
| Merkmal | Server #1 | Server #2 |
|---|---|---|
| Feld für Post-Kategorien | categories | postCategories |
| Feldargument zur Begrenzung der Ergebnisanzahl | first | pagination.limit |
Das Feld id eines Objekts repräsentiert | seine eindeutige globale ID | seine eindeutige ID für seinen Typ |
| Form der Query | tiefer durch edges.node | flacher |
Die Query des ersten Servers allein durch die entsprechende des zweiten Servers in der Anwendung zu ersetzen, wird nicht funktionieren. Das liegt daran, dass die Logik weiterhin auf die Daten der Antwort gemäß der Form und den Feldern der ursprünglichen Query zugreift.
Eine mögliche Lösung besteht darin, auch die Logik zum Abrufen der Daten im Client zu ersetzen. Zum Beispiel kann die folgende Logik:
const categories = data?.data.categories.edges.map(({ node = {} }) => node);...so ersetzt werden:
const categories = data?.data.postCategories;Aber genau das wollen wir vermeiden. Wir möchten die Änderungen auf ein Minimum beschränken, nur die Schnittstelle (die GraphQL-Query) anpassen und die Geschäftslogik unverändert lassen.
Glücklicherweise ist es möglich, die Unterschiede durch ausschließliche Änderung der GraphQL-Queries zu überbrücken, indem wir diese Schritte befolgen:
- Die GraphQL-Queries von der Anwendung trennen
- Die Feldnamen über Aliases anpassen
- Die Form der Antwort über ein
self-Feld anpassen
Schauen wir uns an, wie wir durch diese 3 Schritte eine Anwendung an einen anderen GraphQL-Server anpassen können.
Die GraphQL-Queries von der Anwendung trennen
Das Trennen der GraphQL-Queries von der Anwendungslogik beinhaltet:
- Jede GraphQL-Query (oder eine Gruppe davon) in einer separaten Datei speichern, alle in einem bestimmten Ordner
- Die Queries exportieren und in die Anwendung importieren
Wir können zum Beispiel jede GraphQL-Query in einer separaten Datei unter src/data ablegen und sie exportieren:
// file `src/data/categories.js`
export const QUERY_ALL_CATEGORIES = gql`
{
categories(first: 10000) {
edges {
node {
databaseId
description
id
name
slug
}
}
}
}
`;Die Anwendung kann die GraphQL-Query dann importieren und verwenden:
import { QUERY_ALL_CATEGORIES } from 'data/categories';
export async function getAllCategories() {
const apolloClient = getApolloClient();
const data = await apolloClient.query({
query: QUERY_ALL_CATEGORIES,
});
const categories = data?.data.categories.edges.map(({ node = {} }) => node);
return {
categories,
};
}Dank dieser Einrichtung müssen alle Änderungen ausschließlich an den Dateien unter src/data vorgenommen werden.
Die Feldnamen über Aliases anpassen
Ein Feld-Alias kann verwendet werden, um ein Feld in der Antwort des zweiten GraphQL-Servers mit dem Namen dieses Feldes im ersten Server umzubenennen.
Auf diese Weise können die Felder postCategories, id und globalID mit den von der Anwendung erwarteten Namen abgerufen werden: categories, categoryId bzw. id:
{
categories: postCategories(pagination: { limit: 10000 }) {
categoryId: id
description
id: globalID
name
slug
}
}Beachte, dass das Feld categories das Argument first hat, während das entsprechende Feld postCategories das Argument pagination.limit verwendet. Da die Feldargumente jedoch nicht im Feldnamen der Antwort widergespiegelt werden, müssen wir uns darüber keine Gedanken machen.
Die Form der Antwort über ein self-Feld anpassen
Die letzte Herausforderung ist etwas kniffliger: Wir müssen die Form der Antwort ändern und die zusätzlichen Ebenen für edges und node aus der Cursor Connections Spec hinzufügen.
Um dies zu erreichen, führen wir ein self-Feld in alle Typen des GraphQL-Schemas ein, das dasselbe Objekt zurückgibt, auf das es angewendet wird:
type QueryRoot {
self: QueryRoot!
}
type Post {
self: Post!
}
type User {
self: User!
}Das self-Feld ermöglicht es, der Query zusätzliche Ebenen hinzuzufügen, ohne das abgefragte Objekt zu verlassen. Wenn diese Query ausgeführt wird:
{
__typename
self {
__typename
}
post(by: { id: 1 }) {
self {
id
__typename
}
}
user(by: { id: 1 }) {
self {
id
__typename
}
}
}...wird diese Antwort erzeugt:
{
"data": {
"__typename": "QueryRoot",
"self": {
"__typename": "QueryRoot"
},
"post": {
"self": {
"id": 1,
"__typename": "Post"
}
},
"user": {
"self": {
"id": 1,
"__typename": "User"
}
}
}
}Jetzt können wir self verwenden, um die Ebenen nodes und edge künstlich hinzuzufügen:
{
categories: self {
edges: postCategories(pagination: { limit: 10000 }) {
node: self {
categoryId: id
description
id: globalID
name
slug
}
}
}
}Der Typ des Objekts im GraphQL-Schema für edges und für self ist natürlich unterschiedlich. Das spielt für die Anwendung jedoch keine Rolle, da sie nicht mit dem eigentlichen im GraphQL-Server modellierten Objekt interagiert. Stattdessen empfängt sie die Daten als JSON-Objekt, und dieser Datenteil für ein Feld aus einem PostConnection- oder einem Post-Objekt wird derselbe sein.
Beachte, dass das Feld categories über self aufgelöst wird und edges über postCategories, und nicht umgekehrt. Dies dient dazu, die Kardinalität der zurückgegebenen Elemente entsprechend der durch die Felder der Cursor Connections Spec definierten zu halten:
type RootQuery {
categories: RootQueryToCategoryConnection
}
type RootQueryToCategoryConnection {
edges: [RootQueryToCategoryConnectionEdge]
}
type RootQueryToCategoryConnectionEdge {
node: Category
}Wenn die angepasste GraphQL-Query umgekehrt wäre (d. h. categories: postCategories und edges: self abfragen), würde der Datenzugriff fehlschlagen, weil data.categories ein Array wäre und data.categories.edges bei der Ausführung einen Fehler auslösen würde:
const categories = data?.data.categories.edges.map(({ node = {} }) => node);Alle Queries anpassen
Nachdem dieselbe Strategie auf alle GraphQL-Queries in src/data angewendet wurde, kann die Anwendung problemlos von einem GraphQL-Server zu einem anderen wechseln.