Java-Anwendungen mit GraphQL, Teil 1

Seite 4: GraphQL-Backends in Java

Inhaltsverzeichnis

Um GraphQL-Abfragen zu verarbeiten, bedarf es eines Servers, der eine GraphQL API zur Verfügung stellt. Entwickler verwenden für Java-basierte GraphQL-Anwendungen die Projekte java-graphql-tools und graphql-java-servlet oder graphql-spring-boot, wenn im eigenen Projekt Spring Boot zum Einsatz kommt. Diese Projekte abstrahieren ihrerseits das graphql-java-Projekt, das die eigentliche GraphQL Funktionalität implementiert und auch direkt verwenden werden kann.

Das java-graphql-tools-Projekt stellt die grundlegenden Mittel zu Verfügung, um eine GraphQL-API im eigenen Projekt zu implementieren. Dafür muss man zunächst das Schema der API wie oben gezeigt mit der SDL beschreiben und im Projektverzeichnis ablegen, sodass das GraphQL-Framework es zur Laufzeit einlesen kann. Das vollständige Schema der Anwendung befindet sich im GitHub-Repository.

Im nächsten Schritt müssen Entwickler Resolver implementieren. Das sind Java-Klassen, die sich um das Laden der Daten für ein Feld aus dem Schema kümmern. Für jeden Root Object Type (also Query, Mutation und Subscription), der im Schema vorhanden ist, ist die Implementierung einer eigene Klasse notwendig, die ein Marker-Interface (zum Beispiel GraphQLQueryResolver) implementiert. Innerhalb der Klasse müssen Entwickler für jedes Feld des jeweiligen Root Object Type eine Methode implementieren, die den Wert für das Feld bei einer Anfrage zurückliefert. Sind für ein Feld im Schema Argumente definiert, muss die Methode für jedes der Argumente einen Parameter definieren. Der Rückgabe-Typ der Methode muss ebenfalls dem Schema entsprechen, also ein Objekt zurückliefern, dessen Properties zumindest grundsätzlich den Feldern aus dem Object Type entsprechen.

Für den oben gezeigten Root Object Type Query, der zwei Felder (beer und beers) enthält, könnte der dazugehörige Resolver wie folgt implementiert sein:

public class BeerQueryResolver implements GraphQLQueryResolver {
  private final BeerRepository beerRepository;

  public BeerAdvisorQueryResolver(BeerRepository beerRepository) {
    this.beerRepository = beerRepository;
  }

  public Beer beer(String beerId) {
    return beerRepository.getBeer(beerId);
  }

  public List<Beer> beers() {
    return beerRepository.findAll();
  }
}

Wenn der Server eine Anfrage entgegennimmt, delegiert er die Anfrage zunächst an die Methode in der Resolver-Klasse. Die Klasse ermittelt, welches Objekt (oder Objekte) etwa durch eine Datenbankabfrage zurückgeliefert werden muss. Hat der Client vom zurückgelieferten Objekt Felder abgefragt, sucht eine Reflection am Objekt entsprechende Felder und liefert sie zurück. Die Beer-Klasse kann daher zum Beispiel ein reguläres POJO oder, wie im Beispiel, eine JPA Entity sein.

Resolver für Mutations und Subscriptions implementiert man analog, nur dass sie andere Marker-Interfaces implementieren. Bei Subscriptions ist zu beachten, dass als Rückgabewert der Methode ein Reactive-Streams-Publisher-Objekt zurückgeliefert werden muss.

Das folgende Listing zeigt exemplarisch einen Ausschnitt aus dem Mutation-Resolver, der ein neues Rating entgegen nimmt und in der Datenbank speichert:

public class BeerAdvisorMutationResolver implements GraphQLMutationResolver {
  private final BeerRepository beerRepository;

  public BeerAdvisorMutationResolver(BeerRepository beerRepository) {
    this.beerRepository = beerRepository;
  }

  public Rating addRating(AddRatingInput addRatingInput) {
    Beer beer = beerRepository.getBeer(addRatingInput.getBeerId());

    Rating rating = beer.addRating(
      addRatingInput.getUserId(),
      addRatingInput.getComment(),
      addRatingInput.getStars()
    );

    beerRepository.saveBeer(beer);

    return rating;
  }
}

Über die gezeigten Root-Resolver findet der Einsprung im Objektmodell statt. Der Root-Resolver liefert ein Objekt zurück, das – wie gezeigt – zum Beispiel eine JPA Entity sein kann. Auf die im Objekt abgefragten Felder greift eine Reflection zur Laufzeit zu (vergleichbar etwa mit dem Verhalten von Template- oder Expression-Languages, bei denen häufig eine Punktnotation die Navigation über Objektgraphen ermöglicht).