GraphQL

Eintrag zuletzt aktualisiert am: 24.10.2022

GraphQL ist eine textbasierte Datenabfragesprache zur Verwendung in Webservices/WebAPIs. GraphQL wurde von Facebook entwickelt und 2015 erstmals veröffentlicht. 2016 erschien die erste stabile Version. [https://github.com/graphql/graphql-spec]
Alternative: Open Data Protocol (ODATA)

Beispiel für http://graphql.org/swapi-graphql

GraphQL-Abfrage für Personen mit den Filmen, in denen sie erscheinen

edges {
node {
id, name, filmConnection {
edges {
node {
id, title
}
}
}
}
}
}
}

Spezifikation

https://github.com/graphql/graphql-spec
https://medium.com/@marion.schleifer/einf%C3%Bchrung-in-graphql-88da668ee1

Details zu GraphQL von Jörg Krause

REST ist für kleine und mittlere Umgebungen perfekt. Es ist ausreichend, wenn die Komplexität der Abfragen überschaubar ist. Da jedoch alles eine Ressource ist, wird es unübersichtlich, wenn es sehr viele derartige Ressourcen gibt. An dieser Stelle sind komplexere Abfragesprachen gefragt, die Mehrfachzugriffe und Abhängigkeiten durch entsprechende Syntaxkonstrukte vereinfachen.
Etabliert haben sich hier zwei derartige Standards:
GraphQL basiert auf JSON-Paketen, in denen einen Abfrage formuliert wird. Entsprechend muss fast immer mit POST gearbeitet werden, weil auch bei Abfragen Daten zum Server übermittelt werden. GraphQL erlaubt komplexe Konstrukte und erfordert eine explizite serverseitige Unterstützung. GraphQL ist sehr auf die Web-Welt zugeschnitten und hat in reinen Web-Applikationen Vorteile.

ODATA ist dagegen bei Abfragen URL-basiert und kann GET benutzen. Die Abfragen sind einfacher, vielfältige Filter sind aber auch hier möglich. ODATA ist breiter aufgestellt, wenn es um Umgebungen außerhalb der Web-Welt geht.

Detials zu GraphQL

GarphQL erschien 2015 und hatte von Beginn an starke Unterstützung, weil es eine echte Herausforderung beim Umgang mit REST-Diensten adressiert. Bei komplexeren Systemen, vor allem solchen, die mit Hilfsmitteln wie Swagger definiert werden, explodiert regelmäßig die Anzahl der Endpunkte. Der Dienst wird unübersichtlich und schwer wartbar. Darüber hinaus setzt der Dienst auf eine weitgehende Abstraktion von den Daten und überlässt es dem Client, die Daten zu filtern oder zu sortieren. In Anbetracht starker Datenbanken und großer Datenbestände eine eher schlechte Architektur, die regelmäßig zu Performance-Katastrophen führt.
Ursprünglich in Ruby und Skala geschrieben steht GraphQL als Abfragesprache praktisch auf allen Plattformen zur Verfügung. Immer wenn das führende Format JSON ist, hat GraphQL deutliche Vorzüge gegenüber ODATA, was sich eher im XML-Umfeld wohlfühlt. Die aktuelle Referenzimplementierung ist in JavaScript geschrieben und via npm verfügbar. Der Quellcode ist auf Github zu finden.

Implementierungen

Folgende Umgebungen werden explizit unterstützt:
 C# / .NET
 Clojure
 Elixir
 Erlang
 Go
Groovy
 Java
JavaScript
PHP
Python
Scala
Ruby

Abfragen

Die einfachste Abfrage bei GraphQL fragt nach Feldern eines Objekts. Dies könnte etwa folgendermaßen aussehen:
1 {
2 table_name {
3 field
4 }
5 }
Beachte dabei, dass dies kein gültiges JSON ist – GraphQL ist eine eigene Abfragesprache, die bei Abfragen simplifizierte Konstrukte nutzt. Die Antwort ist dagegen JSON und kann so einfach verarbeitet werden:
1 {
2 "data": {
3 "table_name": {
4 "field": 12345
5 }
6 }
7 }
Neben der vereinfachten Syntax hat GraphQL eine Eigenschaft, die JSON gänzlich fehlt: Kommentare sind erlaubt.
1 {
2 # Die Ressource
3 table_name {
4 # Das Feld
5 field
6 }
7 }

Argumente

Argumente werden direkt an die Namen gesetzt, ohne weitere geschweifte Klammern:
1 {
2 address(id: 1000) {
3 name,
4 city,
5 street
6 }
7 }
Argumente sind auf jeder Ebene möglich, was weitaus flexibler als bei REST ist.
1 {
2 address(id: 1000) {
3 name,
4 city,
5 street,
6 birthday(unit: datetime)
7 }
8 }

Aliase

Ein Feld kann immer nur mit einem Satz Argumente abgerufen werden. Soll dasselbe Feld in unterschiedlicher Weise abgerufen werden, so werden dafür Aliase eingesetzt. Folgende Abfrage bezieht sich auf das Feld hero:
1 {
2 empireHero: hero(episode: EMPIRE) {
3 name
4 }
5 jediHero: hero(episode: JEDI) {
6 name
7 }
8 }
Der Alias empireHero und der Alias jediHero dienen der Unterscheidung. In der Antwort werden die Aliase wiederholt, weil man so die Ergebnisse zuordnen kann:
1 {
2 "data": {
3 "empireHero": {
4 "name": "Luke Skywalker"
5 },
6 "jediHero": {
7 "name": "R2-D2"
8 }
9 }
10 }

Fragmente

Die beim Alias bereits angesprochene einfache Abfragestrategie mit eindeutigen Feldnamen kann bei komplexen Abfragen schnell dazu führen, dass umfangreiche Strukturen wiederholt werden. Dies lässt sich mit Fragmenten entschärfen. Fragmente sind vordefinierte Konstrukte, eingeleitet mit dem Operator … (drei Punkte).
1 {
2 leftComparison: hero(episode: EMPIRE) {
3 …comparisonFields
4 }
5 rightComparison: hero(episode: JEDI) {
6 …comparisonFields
7 }
8 }
9
10 fragment comparisonFields on Character {
11 name
12 appearsIn
13 friends {
14 name
15 }
16 }
comparisonFields ist hier das Fragment, das zweimal (Zeile 3 und Zeile 6) benutzt wird.

Operationen

Bislang wurde davon ausgegangen, dass alles Abfragen sind. Das ist jedoch nicht immer der Fall, denn dynamischere Konstrukte lassen sich mit Variablen bilden. Dazu muss der Dienst aber zwischen einer Definition und einer Abfrage unterscheiden können. Abfragen werden, außer in der bereits gezeigten vereinfachten Form, mit query erstellt.
1 query HeroNameAndFriends {
2 hero {
3 name
4 friends {
5 name
6 }
7 }
8 }
Zulässigen Operationen sind:
 query
 mutation
 subscription
Der Name ist zwingend erforderlich. Er wird jedoch nur zum Loggen auf der Serverseite benutzt, er erscheint nicht in der Antwort und kann deshalb auch nicht zur Referenzierung benutzt werden.

Variablen

Variablen erleichtern die Angabe von sich häufig wiederholenden Daten.
1 query HeroNameAndFriends($episode: Episode) {
2 hero(episode: $episode) {
3 name
4 friends {
5 name
6 }
7 }
8 }
$episode ist hier die Variable. Sie wird in einem separaten Verzeichnis definiert (dort ohne das $-Zeichen):
1 {
2 "episode": "JEDI"
3 }
Der Name für diesen Bereich ist variables:
1 "query": "query ($username: String!){
2 blog {
3 user(username: $username) {
4 username
5 comment
6 }
7 }
8 }",
9 "variables":"{
10 \"username\":\"Joerg\"
11 }"
Dies ist ein Beispiel für serialisiertes JSON, wie es ein Web-Client erzeugt. Die Variable liegt außerhalb der Query-Struktur, aber im selben Paket.
Variablen können Standardwerte haben, die bei fehlender Definition benutzt werden, sodass sich Abfragen allein mittels veränderlicher Variablen robust modifizieren lassen:
1 query HeroNameAndFriends($episode: Episode = "JEDI") {
2 hero(episode: $episode) {
3 name
4 friends {
5 name
6 }
7 }
8 }
Der Vorteil macht sich bemerkbar, wenn man im JavaScript GraphQL-Abfragen zusammenbaut. Dies geht mit Variablen deutlich einfacher als mit den komplexen Zeichenketten-Verknüpfungen, die sonst erforderlich wären.

Direktiven

Direktiven dienen ähnlich wie Variablen dazu, die Abfrage noch dynamischer zu machen und damit den Zusammenbau zu vereinfachen. Folgende Direktiven gibt es:
 @include(if: Boolean)
 @skip(if: Boolean)
Es handelt sich also quasi um Bedingungen. In der folgenden Abfrage bestimmt die Variable withFriends, ob die Eigenschaft friends überhaupt in die Abfrage mit einbezogen wird.
1 query Hero($episode: Episode, $withFriends: Boolean!) {
2 hero(episode: $episode) {
3 name
4 friends @include(if: $withFriends) {
5 name
6 }
7 }
8 }

Mutationen

Mutationen dienen dem Ändern von Daten. Die grundlegende Syntax sieht folgendermaßen aus:
1 mutation CreateReviewForEpisode($ep: Episode!,
2 $review: ReviewInput!) {
3 createReview(episode: $ep, review: $review) {
4 stars
5 commentary
6 }
7 }
Diese Abfrage benötigt folgende Variablen:
1 {
2 "ep": "JEDI",
3 "review": {
4 "stars": 5,
5 "commentary": "This is a great movie!"
6 }
7 }
Hier wird also ein neuer Datensatz erzeugt. Im Ergebnis wird der neue Datensatz zurückgegeben.

Schematas und Typen

Die vorhergehenden Beispiele haben bereits Typen benutzt – sowohl skalare als auch komplexe Typen. Folgende Skalare werden unterstützt:
 Int: Ganzzahl 32 Bit.
 Float: Einfache Gleitkommazahl.
 String: Eine UTF‐8-Zeichenkette.
 Boolean: True oder False.
 ID: Schlüsselwert, wird als Zeichenkette serialisiert
Eigene einfache Typen werden mit scalar erstellt:
scalar Date
Wie ein derartiger Typ behandelt wird, muss in der Implementierung selbst entschieden werden. Ohne weitere Maßnahmen sind dies alles Zeichenketten.
Eigene komplexe Typen können mit type erstellt werden:
1 type Character {
2 name: String!
3 appearsIn: [Episode]!
4 }
Enumerationen (Aufzählungen) sind ebenso möglich, um Wertemengen zu begrenzen:
1 enum Episode {
2 NEWHOPE
3 EMPIRE
4 JEDI
5 }
Um anzuzeigen, dass eine Liste erwartet wird, kann die von JavaScript bekannte Array-Syntax benutzt werden:
field: [String!]
Wird erlaubt, nichts zu übertragen, steht der spezielle Typ null zur Verfügung.
Typen lassen sich von Schnittstellen mit interface ableiten, um die Wartbarkeit komplexer Typsysteme zu erhöhen.
1 interface Character {
2 id: ID!
3 name: String!
4 friends: [Character]
5 appearsIn: [Episode]!
6 }
7
8 type Human implements Character {
9 id: ID!
10 name: String!
11 friends: [Character]
12 appearsIn: [Episode]!
13 starships: [Starship]
14 totalCredits: Int
15 }
16
17 type Droid implements Character {
18 id: ID!
19 name: String!
20 friends: [Character]
21 appearsIn: [Episode]!
22 primaryFunction: String
23 }
Ähnlich wie bei TypeScript lassen sich Typen zu Unions kombinieren. Ein Feld kann dann mehrere Typen aufnehmen, aber keine beliebigen Werte.
1 union SearchResult = Human | Droid | Starship