Nudge am 27.02.2017

Ist GraphQL das bessere REST?

in PHP, Web | Tags: GraphQL, Laravel, PHP, REST

JSON-REST ist der de-facto Standard für Web-Schnittstellen (sogenannte APIs) in unserer heutigen Welt. Eine Welt, die aus so vielen verteilten Diensten besteht. Mal hier das Wetter ziehen, mal dort die Likes und Kommentare auflisten. Es ist schematisch einfach und effektiv. Und als Nutzer eines Dienstes kommt man damit schnell zum Ziel. Braucht man etwas also etwas neues, besseres? Wenn ja, warum? Was ist schlecht an REST? Hier ein kurzer Blick auf GraphQL, eine vielversprechende Alternative zu REST von Facebook.

Im Oktober letzten Jahres kam ich im Rahmen des Developer Open Space zum ersten Mal mit GraphQL in Kontakt, und es hat mir auf Anhieb ein Lächeln ins Gehirn gezaubert. Ich steckte selbst gerade in einem anlaufenden größeren Projekt, in dem eine umfangreiche API für mobile Apps entwickelt werden sollte. Schon zu Beginn der Entwicklung stellten sich immer wieder neue Design-Fragen, die wir nicht ad-hoc beantworten konnten: Wie sieht das optimale Naming und die besten Format-Konventionen aus? Gehören die beiden Datensätze zusammen oder getrennt? Wie werden die Apps den Content abrufen? Wie können wir auf der einen Seite die Anzahl der Requests minimieren, wie jedoch auf der anderen den Umfang der jeweils zu übertragenden Daten?

Sich das Problem bewusst machen

Das Grundproblem ist: Nur der Nutzer einer API kennt seinen optimalen Use Case, seinen genau zugeschnittenen Datenbedarf. Er sollte eigentlich die Anforderungen an die API oder gar die Spezifikation schreiben. Der Hersteller einer API  – in diesem Falle wir – kann diesen nur grob vorausahnen und durch simulierte Workflows abschätzen. Leider muss er gleichzeitig abwägen, wie speziell er auf eine Art von App oder Nutzer eingeht. Denn er möchte ja sicherstellen, dass die API zukunftssicher und erweiterbar sowie generisch für viele Anwendungsfälle nutzbar bleibt.

Bisher haben wir uns diesem Dilemma mit REST eher schlecht als recht ergeben: Wir definieren in REST oft unsere zentralen Objektklassen bzw. Entitäten einfach direkt als Ressourcen, die wir öffentlich zugänglich machen. Die wichtigsten Operationen, z.B. das CRUD-Schema für Create-Read-Update-Delete, bilden die Aktionen, die Clients auf diesen Ressourcen ausführen können. Schlimmer noch: “Leichtgewichtige” Web-Frameworks verleiten uns dazu, mit nur ganz wenigen Annotationen hier und da und ein bißchen Routing-Magic die Ressourcen direkt, quasi 1:1 von den Datenbank-Entitäten zu generieren. Dazu oft Voraussetzung: POJOs, soweit das Auge blickt, mit leeren Konstruktoren, eintönigen Gettern und Settern zu jeder Property. Datencontainer ohne spezifisches Verhalten, schutzlos und ohne Konsistenz jedweder Reflection-Magic von Datenbanktreibern oder API-Bindings ausgeliefert.

1
2
3
4
GET /users
POST /users
GET /user/{id}/articles
POST /comments
GET /users
POST /users
GET /user/{id}/articles
POST /comments

Die Folgen dieser Simplizität

Doch was passiert dann mit unserem “Backend”? Es degradiert. Zu einer etwas spezielleren Form einer JSON-Datenbank wie es z.B. Couch selbst tun könnte. Nun ist das Frontend gefordert, Business-Logik zu implementieren, oder gar der API-Client. Sie müssen die Use Cases orchestrieren, die erforderlichen Objekte anlegen und verknüpfen, Sinn im sinnfreien Raum schaffen. Das ist Inversion-of-Control, wie man sie nicht möchte.

Und auch der Daten-Zuschnitt ist immer falsch, da kann man machen, was man will. Der eine API-User möchte alle “Sub-Daten” einer Entität inline haben, für eine komplexe Seite im Frontend. Der andere möchte ein möglichst schlankes Objekt ohne Schnickschnack. Seine App-User würden für Details eh auf einen Eintrag einer Liste klicken. Diese Probleme heißen Under-Fetching (der Client ist gezwungen, weitere Requests durchzuführen, um subsequente Daten zu erhalten, zum Beispiel alle Kommentare nach dem Abruf eines Artikels) oder Over-Fetching (der Client muss einen Berg an Daten abrufen, nur um ein Detail darzustellen, zum Beispiel den displayName eines Kommentar-Autors darstellen, wenn man seine ID kennt). Auch das Naming ist dabei nicht ohne. Für die einen muss es halt immer name sein, für die anderen am liebsten title, für die dritten eher shortDescription…puh.

Und kommen wir nun mit GraphQL aus diesem Chaos wieder heraus?

Hier kann tatsächlich dieses Ding namens GraphQL helfen. Wie der Name schon sagt, handelt es sich um eine Query Language (QL) für Graphen. Es ist dafür geschaffen, Under- und Over-Fetching aus der Welt zu schaffen, ebenso wie Naming-Probleme. Mit GraphQL wird der Nutzer einer API, der Client, wieder der Designer der Schnittstelle, der seinen optimalen Datenbedarf benennt und bekommt. Und das wir dabei die Kontrolle über unsere Business-Objekte nicht verlieren, können wir auch recht einfach sicherstellen.

Wie das alles funktioniert, schauen wir uns gleich an. Aber erstmal eine andere Frage: Warum überhaupt Graphen? Sind das nicht diese netzartigen Gebilde aus den langweiligen Uni-Vorlesungen? Was haben die denn mit moderner Web-Programmierung zu tun? Die Antwort ist ganz einfach: Wenn wie im obigen Beispiel ein Nutzer ein paar Kommentare verfasst, dann lässt sich das einfach in einem Graphen von einem Knoten-Typ User über Kanten zu Knoten vom Typ Kommentar abbilden. Und von dort mit einer Kante (wie der Eintrag user_id in einer relationen Kommentar-Tabelle) wieder zurück zum Nutzer. Und in diesem Netz lässt uns GraphQL nun wunderbar mit Abfragen “navigieren”. Diese Theorie ist aber nicht so entscheidend. Wir können es einfach bei diesem Begriff belassen und uns direkt mit der Umsetzung beschäftigen.

Was GraphQL nun zunächst definiert, ist eine kleine, leichte aber relativ mächtige Anfragesyntax (QL), die uns per se JSON-Strukturen in gewünschter Form zurückliefern. Und zwar immer vom gleichen Endpunkt. Kein URI-Zusammengeschaube. Klingt so bezaubernd, man mag es kaum glauben. Aber… it’s true (TM), it’s gonna be absolutely fantastic:

Diese Anfrage liefert zum Beispiel alle Nutzer mit ihrem Namen und Email zurück.

1
2
3
4
5
6
{
  users {
    name
    email
  }
}
{
  users {
    name
    email
  }
}

Hätten wir die id des Nutzers gebraucht (zB weil das Profil immer unter /profile/{id} verlinkt werden soll, dann hätten wir sie halt mit angegeben. Brauchen wir aber nicht, also können wir es weglassen, ebenso die potentiellen Felder “active”, “created_at”, “updated_at”, “site_id” und wie sie halt immer alle heißen mögen. Over-Fetching einmal vermieden – check.

Nun wollen wir halt den Namen und die Email neben den Kommentaren eines Artikels anzeigen. Also brauchen wir noch die Kommentare. Hier könnten wir – wenn es die API anbietet – den Graphen vom Nutzer zum Kommentar bemühen und die Kommentare mit abrufen:

1
2
3
4
5
6
7
8
9
10
{
  users {
    name
    email
    comments {
      text
      likes
    }
  }
}
{
  users {
    name
    email
    comments {
      text
      likes
    }
  }
}

Aber halt – das ist doch wieder dieses n:1 oder 1:n Fetching-Problem von REST. Eigentlich will ich doch die Liste der Kommentare mit den Autoren, nicht einen Autor und dessen Kommentare darstellen. Wenn es die API erlaubt – und gute APIs tun das – dann lässt sich der Graph auch andersherum traversieren. Man schaue sich dieses Beispiel an:

1
2
3
4
5
6
7
8
9
10
{
  comments {
    text
    likes_count
    user {
      name
      email
    }
  }
}
{
  comments {
    text
    likes_count
    user {
      name
      email
    }
  }
}

Nun sind die Kommentare führend. Ich erhalte also die Liste der Kommentare, und zu jedem Kommentar den Autor (Sub-Typ “User”) mit seinem Namen und Email (ohne den anderen Schmus).

Das ist cool, oder? Und den Naming-Äger umgehen wir mit einem Alias:

1
2
3
4
5
6
{
  comments {
    content: text
    nlikes: likes_count
  }
}
{
  comments {
    content: text
    nlikes: likes_count
  }
}

Schon heißen diese Felder in meinem Result-Set content und nlikes statt bisher text und likes_count. Sogar kleinere Transformationen sind möglich, zum Beispiel um sich Längen von Fuß in Meter oder ähnlichem umzurechnen. Wahnsinn!

Und es wäre keine Query-Language, wenn man keine Bedingungen angeben könnte. So kann man zum Beispiel den User über die ID abrufen:

1
2
3
4
users(id: 1) {
  name
  email
}
users(id: 1) {
  name
  email
}

Und wie schütze ich nun als Backend-Provider meine Daten-Integrität?

Natürlich muss ich als Entwickler die für die API zur Verfügung stehenden Typen (User, Comment) irgendwie definieren. Und deren Felder und Beziehungen zu anderen Typen. Was ich aber von meinem Einsatz mit dem Laravel-Package aus meiner eigenen praktischen Erfahrung wie auch aus Beobachtung einer .NET-Umsetzung berichten kann, versucht man dies eher durch ein “Data-Binding” zwischen dem API-Layer (GraphQL) und den eigenen Modellen zu realisieren. Das bedeutet, dass man ganz unabhängig von der internen Implementierung oder Klassenaufteilung die GraphQL-API so definiert, wie man seine API sich optimal wünscht. Und dann die Typen und Felder eher lose auf interne Modelle abbildet, sei es auf direktem Wege, oder Binding-Funktionen, die sich berechnen. Mutationen – das sind die “Setter” in GraphQL – schreiben dann nicht direkt in meine eigenen Daten, sondern ich habe im Binding die Möglichkeit, steuernd einzugreifen, per StateMachine zu prüfen o.ä.

Durch dieses funktionale Binding ist es übrigens auch möglich, “virtuelle” Felder anzubieten, zum Beispiel einen fullName, der sich immer aus firstName und lastName errechnet. Dieser muss dann nicht mehr in der DB mitgeschleift und konsistent gehalten werden. Oder man überlegt sich gar komplett virtuelle Typen, die intern eigentlich nur Teil eines anderen sind. Oder man kann ähnliche Objekte API-seitig zusammenlegen sowie Daten aus verschiedenen Quellen zusammenspeisen, die vielleicht aus Performance-Gründen partitioniert wurden. Letzteres war wohl auch ein Design-Ziel von Facebook, damit Frontends nicht zuviele Backend-Quellen anzapfen müssen. So erledigt das ein GraphQL-Controller transparent.

The toping of the cake

Da der GraphQL-Controller der einzige Endpunkt ist, kennt er natürlich auch alle Typen, alle Felder und deren Datentyp. So kann Autovervollständigung und Schema-Prüfung realisiert werden. Gleichzeitig kann er sicherstellen, dass das versprochene Datenschema immer eingehalten wird. Inklusive Null-Prüfung. Nochmals Wahnsinn! Gibt es da noch einen Grund daran zu zweifeln, dass REST zwar okay, GraphQL aber einfach (oder doppelt) besser ist?

Tolle Sache. Wie kann ich das selbst ausprobieren?

Es gibt eine Live-Demo, natürlich auf Star Wars Filmdaten basierend. Es gibt die Graph-API von Github. Als Frontend-Entwickler solltest du dir die Relay-Bibliothek anschauen, die dir Queries per GraphQL gegen einen Endpunkt erlaubt.

Und es gibt die Beispiele dieses Artikels auf Github, umgesetzt in PHP mit dem Laravel-Framework und dem Folkloreatelier-Plugin. Du benötigt dazu PHP 5.6. Starten kannst du das Projekt auch direkt nach dem Auschecken per php -S localhost:8000 im public-Verzeichnis, denn alle enthaltenen Daten sind in-memory. Dann kannst du die Daten per Browser abrufen. Genaueres findest du in der README.

1
2
3
4
$ git clone https://github.com/denudge/graphql-demo.git
$ cd graphql-demo/public
$ php -S localhost:8000 &
$ open http://localhost:8000/graphql?query={users{name,email,comments{text,likes_count}}}
$ git clone https://github.com/denudge/graphql-demo.git
$ cd graphql-demo/public
$ php -S localhost:8000 &
$ open http://localhost:8000/graphql?query={users{name,email,comments{text,likes_count}}}

Viel Spaß beim Ausprobieren!


Das mark ich mir: Alltagz Mr Wong Yigg Del.icio.us Yahoo MyWeb Blinklist Google folkd
 

Leave a Reply

Your email address will not be published. Required fields are marked *