REST in peace...

Hoch optimierte APIs: GraphQL in Serverless-Architekturen integrieren

Michael Dähnert

© Shutterstock / sportpoint

Mit seinen drei Jahren auf dem Markt stellt GraphQL eine gereifte und etablierte Alternative zu REST dar und sollte bei Erstellung oder Weiterentwicklung eines API in Betracht gezogen werden. Verschiedene Anwendungen wie Facebook, Instagram und XING verwenden die REST-Alternative bereits erfolgreich. Grund genug, einen Einblick zu geben, wie GraphQL mit wenig Aufwand in moderne Serverless-Architekturen integriert werden kann. Dazu wird im Zusammenspiel von GraphQL mit AWS Lambda eine hoch skalierbare Implementierung vorgestellt, die auf verschiedene Architekturen und Frameworks adaptiert werden kann.

Wie oft hat man sich als Frontend-Entwickler darüber geärgert, dass ein REST Call nicht alle benötigten Daten geliefert hat. Auch als Backend-Entwickler wurde man jedes Mal wieder von Kollegen darum gebeten, eine weitere Property zu einer Response hinzuzufügen, wenn diese fehlte. Zum Glück gehören diese Probleme dank GraphQL der Vergangenheit an. Während REST hier fest vorgegebene Strukturen für die Rückgabe eines Aufrufs definiert, liefert GraphQL nur die Daten, die im Frontend gewünscht sind. Das Vermeiden von sogenanntem Over- und Underfetching wird erreicht, da mit dem Aufruf der Schnittstelle nicht nur die gewünschte auszuführende Methode genannt wird, sondern auch die gewünschten Rückgabestrukturen.

Wichtige Begriffe des GraphQL-Schemas

GraphQL liefert eine Reihe von Begriffen, die in dessen Schemadefinition verwendet werden. Einige davon werden im Artikel behandelt. Für andere sei auf die GraphQL-Dokumentation verwiesen:

  • Query – lesende Zugriffe auf Daten.
  • Mutation – schreibende Zugriffe auf Daten. Der Aufbau einer Mutation entspricht innerhalb des Schemas dem einer Query, beginnt jedoch mit dem Wort „mutation“.
  • Inline Fragments – Objektbäume können sauber strukturiert und beispielsweise in anderen Queries wiederverwendet werden. Duplicate Code wird so vermieden.
  • Type und InputType – Objekte und ihre Properties sind im Schema fest definiert. Diese Info ist dem Client und dem Server bekannt, wodurch eine Validierung direkt bei Start des Servers und Ausführung eines Requests verwendet werden kann.
  • Scalar – Objekte wie Datumswerte (DateTime) können zu den GraphQL-nativen Elementen wie String, Int und Boolean ergänzt und direkt als Datentyp verwendet werden.
  • Argument/Variable – Bei Übergabe von Serveranfragen können Argumente direkt in die Anfrage geschrieben oder als separate Variablen übergeben werden.
  • Pflichtfelder – werden innerhalb des Schemas mittels nachgestelltem „!“ beschrieben.
  • Direktive – gewünschte Rückgabestrukturen können durch die konditionalen Operatoren if und skip gefiltert werden.

Moderne Entwicklungen in Microservices- und Serverless-Architekturen machen es möglich, hoch skalierbare Systeme zu erstellen. Kombiniert man diesen Vorteil mit GraphQL für netzlastoptimierte APIs, erhält man hoch optimierte, datengetriebene Systeme. Der Artikel gibt einen ersten Einblick in GraphQL. Dabei wird besonders auf das Zusammenspiel mit AWS Lambda als ein Vertreter der Serverless-Architekturen eingegangen.

Ein erster Blick

Wie genau sieht nun der Aufruf eines GraphQL-Servers aus? Der Client erstellt einen JSON Request mit den Elementen query und variables. Der Inhalt des Query-Objekts ist dabei ein String, der die namensgebende Graph Query Language als Wert beinhaltet. Als Variablen werden normale JSON-Objekte von beliebiger Komplexität übergeben. Diese Anfrage wird über einen klassischen POST Request an den Server gesendet. Der Endpoint lautet dabei beispielsweise /graphql. Listing 1 zeigt eine Serveranfrage inklusive Parameter.

{
  "query": "
    query testQuery($id: Int!) {
      getCustomer(id: $id) {
        id
        name
        orders {
          date
        }
      }
    }
  ",
  "variables": {
    "id": 0
  }
}

Das Beispiel könnte nun beliebig erweitert werden, zum Beispiel um den Geburtstag des Kunden, seine Bestell-IDs oder das letzte Log-in. Alles ist möglich, solange die Eigenschaften innerhalb GraphQL als Rückgabe definiert sind. Dies geschieht mittels Schema (Listing 2). Es enthält sämtliche Operationen und Objektstrukturen, mit denen GraphQL arbeiten soll (Kasten: „Wichtige Begriffe des GraphQL-Schemas“).

type Query {
  getCustomer(id: Int!) : Customer
}

type Customer {
  id: Int!
  name: String!
  age: Int
  birthdate: String

  orders: [Order]
}

type Order {
  amount: Int!
  date: String
}

Nachdem die Anfrage auf dem Server verarbeitet wurde, wird die Response zurückgegeben. Sie ist ebenfalls im JSON-Format gehalten und kann somit auch von bereits existierenden Clientimplementierungen gelesen und verarbeitet werden (Listing 3).

{
  "data": {
    "getCustomer": {
      "id": 0,
      "name": "Micha",
      "orders": [
        {"date": "2017-12-21"}, {"date": "2018-02-17"}, {"date": "2018-02-21"}
      ]
    }
  }
}

Einbindung als Java Backend

Nachdem die grundlegende Nutzung eines GraphQLServers bekannt ist, geht es mit der konkreten Implementierung weiter. Im Folgenden wird dazu eine AWS-Serverless-(Lambda-)Funktion mit Anbindung einer NoSQL-Datenbank erstellt. Da AWS Programmiersprachen wie Node.js, Python und Java unterstützt, muss zuerst die Wahl der Programmiersprache getroffen werden. Für das folgende Beispiel wird Java 8 in Verbindung mit der AWS-eigenen NoSQL-Datenbank DynamoDB eingesetzt. Hierbei reicht es aus, ein Maven-Projekt mit folgenden AWS und GraphQL Dependencies zu erstellen (Listing 4).

<dependencies>
  <!-- GraphQL dependencies -->
  <dependency>
    <groupId>com.graphql-java</groupId>
    <artifactId>graphql-java</artifactId>
    <version>7.0</version>
  </dependency>
  <dependency>
    <groupId>com.graphql-java</groupId>
    <artifactId>graphql-java-tools</artifactId>
    <version>4.3.0</version>
  </dependency>

  <!-- AWS dependencies -->
  <dependency>
    <groupId>com.amazonaws</groupId>
    <artifactId>aws-lambda-java-core</artifactId>
    <version>1.2.0</version>
  </dependency>
  <dependency>
    <groupId>com.amazonaws</groupId>
    <artifactId>aws-java-sdk-dynamodb</artifactId>
    <version>1.11.280</version>
  </dependency>
  <dependency>
    <groupId>com.google.code.gson</groupId>
    <artifactId>gson</artifactId>
    <version>2.8.2</version>
  </dependency>
</dependencies>

Als Endpunkt für Aufrufe dient eine Methode mit zwei Parametern, dem Input- und dem Context-Parameter:

public String handleRequest(InputType input, Context context)

Der Input-Parameter wurde bereits für GraphQL optimiert. AWS Lambda deserialisiert den erhaltenen JSON Request automatisch in die genannten Objekte. Für GraphQL reichen hierbei die beiden folgenden Properties aus:

class InputType {
  String query;
  Map<String, Object> variables;
  ...
}

Start-up der Serverless-Funktion

Beim Starten der Lambda-Funktion wird das GraphQLSchema initial analysiert und die entsprechenden Java-Handler werden verdrahtet. Dazu muss das Schema bei Erstellung mit den entsprechenden Informationen gefüllt werden. Drei Aspekte sind hierbei wichtig:

Erstens das Parsen und Validieren des Schemas; hierdurch werden Syntaxfehler bereits beim Startvorgang erkannt:

SchemaParserBuilder parser = SchemaParser.newParser().file("schema.graphqls");

Zweitens das Einrichten der Java Resolver; diese Klassen beinhalten die spätere Businesslogik:

parser.resolvers(new QueryResolver());
GraphQLSchema schema = parser.build().makeExecutableSchema();

Drittens die Übergabe der Daten an den GraphQL-Service – die übergebenen Parameter werden von GraphQL geparst und die entsprechende Businesslogik wird aufgerufen:

ExecutionInput exec = ExecutionInput.newExecutionInput()
  .query(input.getQuery())
  .variables(input.getVariables())
  .build();

Die Ergebnisse können später direkt als Response verwendet werden:

return GraphQL.newGraphQL(schema).build()
  .execute(exec)
  .toString();

Workflow der Requests

Nachdem die Query an GraphQL übergeben wurde, wird die aufzurufende Methode geparst und ermittelt. Des Weiteren werden die übergebenen Parameter automatisch validiert und in die entsprechenden Java-Objekte konvertiert. Damit hat der GraphQL-Service an dieser Stelle seinen Dienst getan. Nun kann sämtliche gewünschte Java-Funktionalität in bekannter Form ausgeführt werden. Listing 5 stellt die Verbindung zu einer DynamoDB-Tabelle her und liest ein Kundenobjekt aus. Die Besonderheit hierbei ist: Werden die Lambda-Funktion und die erstellte DynamoDB-Tabelle in einem AWS-Account betrieben, ist es ausreichend, die AWSRegion und den Tabellennamen als Connection-Parameter anzugeben.

public class QueryResolver implements GraphQLQueryResolver {

  public Customer getCustomer(int id) {
    return getDB().load(Customer.class, id);
  }

  private DynamoDBMapper getDB() {
    AmazonDynamoDBClientBuilder builder = AmazonDynamoDBClientBuilder.standard();
    builder.withRegion(Regions.EU_CENTRAL_1);

    return new DynamoDBMapper(builder.build());
  }

}

Beim Arbeiten mit den DynamoDB-Objekten kann natürlich in gewohnter POJO-Manier verfahren werden (Listing 6). Hierzu bietet AWS an JPA angelehnte Annotationen an, die bei der Kommunikation mit der Datenbank Rückgabewerte in Java-Objekte konvertieren.

@DynamoDBTable(tableName = "customer")
public class Customer {
  @DynamoDBHashKey(attributeName = "id")
  public Integer getId() { return id; }

  @DynamoDBAttribute(attributeName="name")
  public String getName() { return name; }

  @DynamoDBAttribute(attributeName="orders")
  public List getOrders() { return orders; }

  [...]
}


@DynamoDBDocument
public class Order {
  @DynamoDBHashKey(attributeName = "id")
  public Integer getId() { return id; }

  @DynamoDBHashKey(attributeName = "date")
  public String getDate() { return date; }

  [...]
}

Sobald die Verarbeitung des Requests beendet ist, werden die Ergebnisse direkt an den ausführenden Graph-QL-Service übergeben. Hier kommt nun der Mehrwert von GraphQL zum Tragen. Wir erinnern uns, dass der Request lediglich ID, Name und Bestellungen des Kunden angefragt hat. Das Customer-Objekt hingegen beinhaltet zusätzliche Eigenschaften. Wird dieses Objekt nun übergeben, so entfernt der GraphQL-Service die ungewünschten Eigenschaften und ignoriert Strukturen, sodass an den Client lediglich die angefragten Elemente geliefert werden.

Es sei noch kurz auf schreibende Anfragen (Mutationen) hingewiesen. Diese laufen nach dem gleichen Schema wie Queries ab, werden in der Anfrage lediglich durch das Schlüsselwort „mutation“ gekennzeichnet. Funktionserweiterungen benötigen in GraphQL lediglich drei Aktualisierungen.

Erstens Schema erweitern:

type Mutation {
  addOrder(newOrder: OrderInput!) : Order
}

input OrderInput {
  customerId: Int!
  amount: Int!
}

Zweitens Handler registrieren:

parser.resolvers(new MutationResolver());

Drittens Businesslogik implementieren:

public Order addOrder(OrderInput newOrder) {
  Customer c = getDB().load(Customer.class, newOrder.getCustomerId());
  Order o = new Order();

  o.setAmount(newOrder.getAmount());
  o.setDate(DateTime.now().toDateTimeISO().toString());

  c.getOrders().add(o);
  getDB().save(c);

  return o;
}

Tabelle 1 können die Beispielanfrage und die Rückgabedaten entnommen werden. Man sieht in der Tabelle gut, dass der Aufbau einer Mutation der einer Query sehr ähnelt.

Tabelle 1: Beispiel-Mutation: Request und Response

Tabelle 1: Beispiel-Mutation: Request und Response

Deployment und Eindrichtung von AWS Lambda

Damit die Lambda-Funktion innerhalb von AWS ausgeführt werden kann, fehlt noch ein kleines Detail. Das Maven-Projekt muss als sogenannte Fat-JAR kompiliert werden. Dazu bietet Maven die Möglichkeit, über das Shade-Plug-in zu arbeiten. Dadurch werden alle benötigten Dependencies in einem einzigen, nach AWS deploybaren, Artefakt gebündelt. Mit der Ausführung von mvn clean package wird nun das Artefakt erstellt und kann nach AWS hochgeladen werden. Lambda ist damit arbeitsfähig. Soll sie mit einem Client wie Angular zusammenarbeiten, muss sie nur noch via API-Gateway mit dem Internet verbunden werden und entsprechende Rollen und Rechte erhalten. An dieser Stelle sei auf die ausführliche AWS-Dokumentation zur Erstellung eines API-Gateways mit Proxy-Integration einer Lambda verwiesen.

Zusammenfassung und Fazit

Das aufgezeigte Beispiel kann gut als Einstiegspunkt zur Verwendung von GraphQL in Java genutzt werden. Durch die Implementierung innerhalb einer Serverless-Anwendung ist darüber hinaus eine hoch skalierbare Verwendung garantiert. In GraphQL können multiple Methoden in einem einzigen Request gebündelt werden. Des Weiteren existieren bereits Bibliotheken, die die Integration einer GraphQL-Schnittstelle nahtlos mit Spring Boot ermöglichen, sowie diverse Implementierungen unter anderem für Angular, Node.js und Python bieten. Dadurch ist eine nahtlose Verwendung in Server- und Clientanwendungen garantiert.

Aufgrund seines Designs lässt sich GraphQL sehr einfach in vorhandene Architekturen integrieren. Da es lediglich ein Wrapper zwischen angefragten Daten und der Businesslogik darstellt, ist es ein Leichtes, bekannte Datenbanken wie MySQL und Oracle mittels JPA anzubinden. Dank dieser Flexibilität kann es sehr gut neben bereits existierenden REST-APIs eingebunden werden, um somit hoch optimierte Anfragen an Backend-Services zu garantieren.

Dem geneigten Leser seien noch zwei Links ans Herz gelegt: Zum einen kann der artikelbegleitende Democode auf GitHub unter https://github.com/mdaehnert/graphql-serverless-demo eingesehen werden. Zum anderen ist die Homepage von GraphQL sehr zu empfehlen, da hier viele weitere Ideen zur Verwendung von GraphQL aufgezeigt werden: http://graphql.org/.

Geschrieben von
Michael Dähnert
Michael Dähnert
Michael Dähnert ist Senior Solutions Architect bei Sopra Steria Consulting. Seit über zehn Jahren arbeitet er in verschiedensten IT-Projekten. Zu seinen Spezialaufgaben zählt, IT-Architekturen zu bewerten, zu optimieren und neu zu konzipieren.
Kommentare

Hinterlasse einen Kommentar

Hinterlasse den ersten Kommentar!

avatar
400
  Subscribe  
Benachrichtige mich zu: