Java-Anwendungen mit GraphQL, Teil 1

Abfragen mit GraphQL ausführen

Wie beschrieben ist die zentrale Idee von GraphQL, genau bestimmte Daten vom Server zu lesen. Dazu führt GraphQL Abfragen aus, die aus einer oder mehreren Operationen bestehen. Eine Operation kann das Lesen von Daten sein (Query), die Veränderung von Daten auf dem Server (Mutation) oder das Registrieren für Events, die der Server auslöst und an den Client schickt (Subscription).

Abfragen in GraphQL bestehen immer aus einer Menge an Feldern, die die GraphQL-Schnittelle auf dem Server liest. Darin können Anwender eine ganze Hierarchie von Feldern aus Objekten erfragen. Die Abfrage sieht ähnlich wie ein JSON-Objekt aus, ist aber ein regulärer String. Das folgende Beispiel zeigt die Query, die Namen und durchschnittliche Bewertung für alle Beer-Objekte lädt, um die Übersichtsseite der Anwendung darzustellen:

query OverviewPageQuery {
beers {
name
averageStars
}
}

Die Detailansicht eines Biers in der Anwendung benötigt andere Daten, beispielsweise die Bewertungen und von den Bewertungen wieder den Autor. Eine Query kann solche Informationen ebenfalls herausfinden. Neu ist im folgenden Listing, dass der Code Felder aus einer Hierarchie von Objekten abfragt. Außerdem erhält ein Feld (beer) ein Argument (ähnlich wie in Java bei Methoden).

query BeerViewQuery {
beer(beerId: "B1") {
name
ratings {
comment
author {
name
}
}
shops { . . . }
}
}

Damit der Client die Query für unterschiedliche Werte nicht jedes Mal neu zusammenbauen muss, kann die Query ebenfalls Argumente entgegennehmen und an ein oder mehrere Felder innerhalb der Abfrage weiterreichen. Im folgenden Beispiel erwartet die Query die Id eines Beer-Objekts, die an das beer-Feld weitergereicht wird. Dank der Verwendung von Argumenten muss der Client den Query-String nur einmal zusammenbauen, unabhängig davon, welche Werte die einzelnen Felder abfragen. Der Server erhält die Werte für die Query (im Beispiel unten: $beerId) in einem gesonderten Objekt. Somit sind auf dem Server Optimierungen denkbar – wie für das Parsen der Query.

query BeerViewQuery($beerId: ID!) {
beer(beerId: $beerId) {
... alles weitere wie oben ...
}
}

Eine Abfrage in GraphQL besteht folglich aus einer hierarchischen Struktur von Feldern. Ein GraphQL-fähiger Server erhält die Abfrage. Das erfolgt in der Regel per HTTP-POST-Request an einen zentralen Endpunkt. Dieser nimmt die Anfrage entgegen, verarbeitet sie und liefert das Ergebnis zurück. Das folgende Kommando zeigt beispielhaft, wie man mit dem Kommandozeilen-Tool curl eine GraphQL-Abfrage ausführen kann.

curl -X POST -H "Content-Type: application/json" \
-d '{"query":"{ beers { id name } }"}'
http://localhost:9000/graphql

Ergebnis der Abfrage

Das Ergebnis der Abfrage ist ein JSON-Objekt, das laut Spezifikation auf Root-Ebene ein data-Feld und/oder ein errors-Feld enthalten muss. Fehler, die bei der Ausführung aufgetreten sind, landen im Feld errors. Das data-Feld enthält bei erfolgreicher Ausführung die abgefragten Daten, deren Hierarchie identisch mit der Abfrage ist. Der Client weiß daher durch die Formulierung seiner Abfrage, wie die Antwort vom Server strukturell aussieht. Eine exemplarische Antwort auf die oben gezeigte BeerViewQuery ist in der folgenden Abbildung zu sehen – die Hierarchie unterhalb von data entspricht genau der abgefragten Struktur der Query.

Die Antwort eines GraphQL-Query entspricht von der Struktur her der Abfrage (Abb. 2).

Möchte der Client Daten auf dem Server verändern (oder erzeugen oder löschen), führt er eine Mutation aus. Die Abfrage dazu ist fast identisch zu einer Query, nur dass sie als Operation-Typ mutation angibt. Der Client übergibt der Mutation alle Daten, die der Server benötigt, um die Aktion erfolgreich auszuführen. Aus dem spezifischen Ergebnis einer Mutation kann der Client wiederum – genau wie bei einer Query – einzelne Felder auswählen, die er vom Server als Antwort benötigt. Im Beispiel können Nutzer über eine Mutation eine neue Bewertung (Rating) für ein Bier abgeben. Dazu muss der Client Informationen wie den Kommentar und den Benutzer übergeben. Für die Antwort wählt der Client lediglich das id-Feld aus (das auf dem Server für das neue Rating-Objekt generiert wurde).

mutation AddRatingMutation {
addRating(beerId: "B1", userId: "U2", comment: "tasty", stars: 3) {
id
}
}

Auch der dritte Operation-Typ, die Subscription, arbeitet nach dem gleichen Muster. Die Besonderheit ist, dass der Server nicht einmalig Daten an den Client zurückschickt, sondern, solange die Verbindung besteht beziehungsweise die Subscription nicht abgebrochen wird, dem Client immer neue Daten liefert, sobald sie auf dem Server zur Verfügung stehen. Der Vollständigkeit halber zeigt das nächste Listing noch eine Subscription, mit der sich ein Client über neue Bewertungen informieren lassen kann. Der Client kann wählen, welche Felder er beim Eintreffen eines Events benötigt, sodass er nur die Daten erhält, die für ihn relevant sind:

subscription RatingSubscription($beerId: ID!) {
rating: newRatings(beerId: $beerId) {
id
stars
beer {
id
}
author {
name
}
comment
}
}