Rund um: GraphQL

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.

PostGraphile in-depth


Für neue Blogupdates anmelden:


Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.