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)
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 …comparison
Fields
4 }
5 rightComparison: hero(episode: JEDI) {
6 …comparison
Fields
7 }
8 }
9
10 fragment comparison
Fields on Character {
11 name
12 appearsIn
13 friends {
14 name
15 }
16 }
comparison
Fields 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 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 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