Im ersten Teil unserer PostgraPhile-Serie haben wir euch ein Setup an Technologien vorgestellt, das gut zusammenspielt: PostgreSQL, PostGraphile, GraphQL und TypeScript.
In diesem Artikel beleuchten wir die Tiefen der GraphQL-Sprache mit ihren Möglichkeiten und Eigenheiten.
Rund um GraphQL
GraphQL wurde im Jahr 2012 von Facebook entwickelt und 2015 veröffentlicht. Facebook hat 2019 die GraphQL Foundation als neutrale non-profit Organisation – unter dem Dach der Linux Foundation – für die Weiterentwicklung und Kooperation gegründet.
Der GraphQL-Standard wird von eben dieser GraphQL Foundation vorangetrieben. Alle wichtigen Hintergründe und die Release Notes findet man auf spec.graphql.org.
GraphQL hat sein größtes Toolset im JavaScript-Umfeld (z. B. graphql-tools), dennoch ist das Toolset in anderen Sprachen mittlerweile auch auf einem ähnlichen Stand. Es gibt ein paar Plattformen wie z. B. Hasura oder PostGraphile, die direkt aus Datenbanken das Schema und die Service-Implementierung generieren. Ein Überblick über Partner, Projekte und Produkte findet man im GraphQL Landscape.
GraphQL: Die Sprache
Sehen wir uns zunächst einmal die Sprache an.
Dokument & Operationen
In GraphQL werden Anfragen an einen Endpunkt als Dokument definiert. Ein Dokument kann dabei mehrere Operationen enthalten.
Es gibt drei Operationen: query
für die Daten-Selektion, mutation
für die Daten-Manipulation und subscription
für Live-Daten. Wenn nur eine query
-Operation definiert wird, darf der Operationsbezeichner weglassen werden.
Operationen können auch benannt werden. Das muss immer dann gemacht werden, wenn mehrere Operationen in einem Dokument existieren.
Hier findet ihr Beispiele für ein query
, eine mutation
und eine subscription
:
query SomeUsers { users { name age } }
mutation ChangeUsers { changeUser(input: { id: 1, age: 54}) { age } }
subscription NortifyMeOnUserChange { users { name age } }
Felder und Argumente
Operationen können auch Argumente definieren, die in den Anfragevariablen mitgeliefert werden müssen. Das ist nützlich, da manche Felder Argumente für die Selektion der Daten entgegennehmen. Dabei kümmert sich der Service dann um das Escaping innerhalb des Dokuments bei Substituierung der Variablen.
Wichtig ist, dass der Typ einer Variable angegeben werden muss. Was die Typen bedeuten, wird weiter unten erklärt.
Beispiel für Argumente
mutation ChangeUsers($id: ID!,$age:number!) { user(input: {id: 1, age: 54}) { age } }
Felder können auch als ein Alias auf ein anderes Feld angegeben werden. Das kann nützlich sein, um ein Feld mit unterschiedlichen Argumenten in einem „Selection Set“ zurückzugeben, wie in dem Beispiel unten für Profilbilder zu sehen.
Anhand dieses Beispiels sieht man schon, wie flexibel GraphQL eingesetzt werden kann – denn hier definiert der Konsument was er möchte.
query { user(id: 4) { id name smallPic: profilePic(size: 64) bigPic: profilePic(size: 1024) } }
Selection Set und Fragmente
In einer Operation müssen Felder in einem sogenannten „Selection Set“ definiert werden. Ein Feld kann auch ein „Selection Set“ beinhalten, wenn das Feld eine Beziehung darstellt.
Ein „Selection Set“ beginnt mit einer geschweiften Klammer {
und endet mit einer geschweiften Klammer }
, wie in dem Beispiel unten zu sehen ist.
query { me { id firstName lastName birthday { month day } friends { name } } }
Um Dokumente möglichst leserlich und konsistent zu halten, gibt es die Möglichkeit, Fragmente zu verwenden. Als Beispiel haben wir ein Dokument für das Abfragen und Ändern des Geburtsdatum eines Benutzers.
Indem wir ein Fragment einsetzen, halten wir beide Operationen synchron was die Datenabfrage angeht, um das Caching zu erleichtern. Fragmente können für Typen, Interfaces und Unions definiert werden. Auf das Typensystem gehen wir noch weiter unten ein.
query User($id: ID!){ user(id:$id) { ...userFragment } } mutation changeUserBirthday($id: ID!,$newBirthday: Birthday!){ user(input: { id: $id, birthday: $newBirthday }) { ...userFragment } } fragment userFragment on User { id firstName lastName birthday { month day } friends { name } }
Fragmente können auch inline definiert werden. Das wird verwendet, wenn auf einem Union-Feld unterschiedliche Felder selektiert werden sollen.
Als Beispiel liefert das Feld content
einen Union-Typ aus Blogpost, Video und Stream zurück, auf dem unterschiedliche Felder existieren. Auf das Typensystem gehen wir wie gesagt weiter unten ein.
Um die Einträge auseinander halten zu können, selektieren wir noch das Feld __typename
. Es beinhaltet den Typ-Namen als String und wird automatisch im Schema von der GraphQL-Engine hinzugefügt.
query Content { content { __typename id title ... on Blogpost { read } ... on Video { views likes } ... on Stream { views beginn end } } }
Direktiven
Direktiven bieten die Möglichkeit, über programmatische Erweiterungen dynamisch einheitliches Verhalten abzubilden, ohne es an jedem Feld zu implementieren. Dies kann auf Operationen, Feld- und Schemaebene angegeben werden.
Einen Überblick der Build-in-Direktiven können in den Specs nachgeschlagen werden. Als Beispiel zeigen wir hier die Build-in-Direktive @include
, die dafür sorgt, dass ein Fragment oder Feld nur in das „Selection Set“ aufgenommen wird, wenn der Übergabe-Parameter if
true ist.
query inlineFragmentNoType($expandedInfo: Boolean) { user(handle: "zuck") { id name ... @include(if: $expandedInfo) { firstName lastName birthday } }
Typensystem
Das GraphQL-Typensystem beschreibt den Funktionsumfang eines GraphQL-Services.
Es wird verwendet, um zu entscheiden, ob die gewünschte Operation gültig ist und um zu garantieren, dass die Typen sowohl zur Anfrage- als auch zur Antwortzeit richtig sind. Dabei wird sowohl die Anfrage als auch die Antwort gegen das Schema geprüft und bei Typabweichungen eine Fehlermeldung zurückgegeben.
GraphQL bietet eine Vielzahl unterschiedlicher Arten von Typen. Dazu zählen einfache Typen wie Scalare und Enums, sowie komplexere Typen wie Objekte, Listen, Interfaces und Unions.
Dabei wird immer unterschieden, ob es erlaubt ist, dass der Typ Null sein darf oder nicht. Dies wird mit einem !
angegeben. Wenn !
angegeben wird, kann sich drauf verlassen werden, dass dieser Wert nie null sein wird.
Bei Listen wird bei der Nullability zwischen dem Feld und den Elementen in der Liste unterschieden. In unserem Beispiel darf das Feld selber null werden, die Elemente allerdings nicht.
type User { id: ID! birthday: Birthday friends: [User!] }
GraphQL IDL
GraphQL stellt eine IDL (Interface Definition Language) bereit, die es Tools ermöglicht sowohl Clients als auch Service-Bootstrap-Code zu generieren.
Das ermöglicht es, die Schnittelle zu planen ohne programmieren zu müssen und ohne auf Sprach- oder Platform-Spezifika eingehen zu müssen.
Bei Änderungen können beide Seiten (Client/Service) bequem und schnell daraus wieder ihren sprach- und platform-spezifischen Code generieren.
Schema, Introspection und Root-Operation-Types
Jedes GraphQL-Schema beginnt mit dem Schema-Feld. In diesem werden die drei Operations-Felder mit deren Typen definiert. Dabei müssen jedoch nicht alle Operationen definiert werden.
Innerhalb eines Schemas müssen alle Typen einen eindeutigen Namen haben und dürfen nicht mit den internen Typen kollidieren.
Hier ein Beispiel einer Schemadefinition:
schema { query: Query mutation: Mutation subscription: Subscription } type Query { ..... } type Mutation { ..... } type Subscription { ..... }
Mit dem folgenden Dokument kann man das Schema, das von einem GraphQL-Service bereitgestellt wird, selbst anfragen.
Dabei handelt es sich um ein sogenanntes Schema-Introspection-Query. Neben diesem gibt es noch die Möglichkeit, sich Informationen über Typen, Felder, Input Objekte, Enums und Direktiven zu selektieren.
Weitere Informationen können in den Spezifikationen gefunden werden.
{ __schema { description queryType { name description } mutationType { name description } subscriptionType { name description } } }
Eingabe- und Ausgabetypen
GraphQL unterscheidet zwischen Eingabe- und Ausgabetypen.
Als Eingabetypen (Argumente) dürfen nur Scalare, Enums oder ein spezieller Input-Typ angegeben werden. Diese dürfen auch als Liste oder Non-Null angegeben werden.
Ein Ausgabetyp darf alles außer einem Input Typ sein.
Scalare
In GraphQL sind diese Scalare Int, Float, String, Boolean und ID build-in.
Genauere Spezifikationen der Scalare können im entsprechenden Bereich der Spezifikationen nachgeschlagen werden. Allerdings können auch eigene Scalare definiert werden.
Objekte
Während Scalare quasi nur die Blätter des Graphen beschreiben, können wir mit Objekten die Äste und Stämme beschreiben. Dabei setzen sich Objekte aus Scalaren, Enums, Objekten, Interface oder Union Feldern zusammen.
Hier ein Beispiel eines einfachen GraphQL Schema:
schema { query: Query } type Query { user(id: ID!) users: [User!] } type User { id: ID! name: String! surname: String! birthday: Birthday } type Birthday { day: Number! month: Number! year: Number! }
Interfaces
Interfaces bestehen aus einer Liste aus Feldern, die von Objekten oder Interfaces implementiert werden können. Dabei kann ein Objekt oder Interface mehrere Interfaces implementieren.
Hier ein Beispiel, um benannte Objekte und Objekte mit Adressen abzubilden:
interface AddressableEntitiy { street: String! houseNumber: Number! zip: String! town: String! } interface NamedEntity { name: String! } type Person implements NamedEntity & AddressableEntitiy{ name: String! street: String! houseNumber: Number! zip: String! town: String! } type Business implements NamedEntity & AddressableEntitiy{ name: String! street: String! houseNumber: Number! zip: String! town: String! }
Der Vorteil davon, Interfaces zu verwenden, liegt darin, dass sich in Dokumenten auf diese Interfaces ein Fragment definieren lässt, welches in jeder Anfrage verwenden werden kann, in der eines der implementierenden Objekte verwendet wird.
Damit kann sichergestellt werden, dass Änderung am Interface automatisch bei jeder Verwendung des Interface-Fragments ankommen, ohne jede einzelne Stelle anfassen zu müssen.
Beispielsweise können sich UI-Komponenten an den Interfaces orientieren, die aus der Codegenerierung herausfallen, und so automatisch Anzeigen für diese Objekte bereitstellen.
Unions
Unions sind eine Liste an möglichen Typen, die das Objekt sein kann. Über das Feld __typename
kann in einem „Selection Set“ der tatsächliche Typ selektiert werden.
Hier ein Beispiel anhand einer Suche – das SearchResult
kann sowohl ein Photo
als auch eine Person
sein.
union SearchResult = Photo | Person type Person { name: String age: Int } type Photo { height: Int width: Int } type SearchQuery { firstSearchResult: SearchResult }
Enums
Enums sind Skalare, die nur vorbestimmte Werte annehmen können. Sie werden immer als Text serialisiert.
Hier ein Beispiel einer Enum-Definition von Himmelsrichtungen:
enum Direction { NORTH EAST SOUTH WEST }
Typ-Extensions
GraphQL ermöglicht es, Typen nach der Definition zu ändern ohne die ursprüngliche Definition ändern zu müssen. Das wiederum ermöglicht es z. B. über Plugins Typen anzureichern, bis hin zum „Mergen“ mehrerer GraphQL-Services zu einem Service mittels sogenanntem Schema-Stitching.
Das ist eine große Stärke im Microservice-Umfeld, da sich so mehrere Microservices einfach zu einem API zusammenfassen lassen, ohne dass Typsicherheit verloren geht und durch die Zusammenfassung Komplexität entsteht.
Aussicht
Mit diesem Artikel haben wir euch ein tieferes Verständnis für GraphQL, mit dem wir unser API erstellen, vermittelt. Im nächsten Teil der Serie werfen wir einen Blick auf PostGraphile und zeigen unter anderem, was PostGraphile eigentlich aus einer Tabelle generiert. Dort stellen wir euch auch ein Repository zur Verfügung, mit dem ihr selbst ein wenig experimentieren und die einzelnen Teile der Serie besser nachvollziehen könnt.
Schreibe einen Kommentar