Spring for GraphQL in der Praxis: Eine GraphQL-API für die Tierklinik

Seite 2: Daten ermitteln mit Handler-Funktionen

Inhaltsverzeichnis

Damit das Backend die Daten für eine Query zurückliefern kann, muss die entsprechende Funktionalität implementiert sein. Die grundlegende Idee ist bei nahezu allen GraphQL-Frameworks unabhängig von der verwendeten Programmiersprache gleich: es gibt pro Feld im Schema eine Resolver-Funktion (in den Java-basierten Frameworks auch Data Fetcher oder Handler-Funktion genannt). Wenn eine GraphQL Query am Backend ankommt, wird sie vom Framework zunächst geparst und gegen das Schema validiert. Nur syntaktisch korrekte Queries werden dann verarbeitet. Dazu ruft das Framework im einfachsten Fall für alle abgefragten Felder nacheinander die Resolver-Funktionen auf. In der Praxis ist es allerdings meist nicht nötig, für alle Felder derartige Funktionen zu schreiben, da Spring-GraphQL zum Beispiel Daten auch per Reflection aus einer Entity-Klasse oder einer Map ermitteln kann. Zwingend erforderlich sind jedoch Resolver-Funktionen für alle Felder der Root-Typen.

In Spring-GraphQL sind dazu Handler-Funktionen zu schreiben und in einer Klasse zu implementieren, die mit der von Spring bekannten @Controller-Annotation markiert ist. Die einzelnen Handler-Funktionen erhalten ihrerseits eine @QueryMapping-Annotation und heißen im einfachsten Fall wie das Feld im Schema, dessen Wert sie ermitteln sollen.

Eine sehr einfache Implementierung für das owners-Feld am Query-Typen zeigt das folgende Listing:

Listing: Handler-Funktion für das owners-Feld

@Controller
public class OwnerController {
  private OwnerRepository repository;
  public OwnerController(OwnerRepository repository) {
    this.repository = repository;
  }

  @QueryMapping
  public List<Owner> owners() {
    return repository.findAll();
  }
}

Die Handler-Funktion verwendet das Spring Data Repository, um alle Owner aus der Datenbank zu lesen und zurückzugeben. Diese Implementierung wäre in der Praxis sicherlich zu trivial, da sie unter anderem die Anzahl der zurück gelieferten Objekte in keiner Weise begrenzt, zeigt aber die grundsätzliche Funktionsweise einer Handler-Funktion.

Aus den zurückgelieferten Owner-Entitäten ermittelt Spring GraphQL nun per Reflection die vom Client abgefragten Felder. Da im Beispiel der PetClinic-API die Felder am Schema genauso heißen wie die Felder (bzw. getter-Methoden) an den Entity-Klassen, sind an dieser Stelle keine weiteren Maßnahmen nötig, und die oben stehende Query lässt sich unmittelbar ausführen. Dazu stellt Spring GraphQL automatisch einen HTTP-Endpunkt über Spring MVC oder Spring WebFlux zur Verfügung, an den Clients die Query schicken können.

In der PetClinic ist die Verwaltung der Tierärzte (Vets) in einem eigenen Microservice abgelegt. Da es keine Beschränkungen gibt, was in einer Handler-Funktion implementiert ist, lassen sie sich auch nutzen, um auf externe Services zuzugreifen. Daher lässt sich das Schema um ein weiteres Feld im Query-Typen erweitern, um darüber einen Tierarzt anhand seiner ID abzufragen:

Listing: Das vet-Feld am Query-Typ

type Query {
  owners: [Owner!]!
  """
    Return the Vet with the given id or null, 
    if no such Vet exists
   """
  vet(id: ID!): Vet
}

Da eine Handler-Funktion ein Mono- oder Flux-Objekt aus dem Reactor-Projekt zurückliefern kann, steht der Implementierung auch der reaktive Spring WebClient zur Verfügung, um die angefragten Daten aus dem Microservice zu lesen. Auf das Argument, dass der Client an den Server sendet, um anzugeben, welcher Tierarzt gelesen werden soll, kann die Handler-Funktion über die @Argument-Annotation kommen:

Listing: Laden von Daten aus einem externen Service

@Controller
public class VetController {
  private WebClient webClient;

  public VetController() {
    this.webClient = WebClient.builder()
     .baseUrl("...")
     .build();
  }

  @QueryMapping
  public Mono<VetResource> vet(@Argument("/vets/{id}") int id) {
    return webClient.get()
      .uri(b -> b
        .path("/vets/{id}")
        .build(visit.getVetId()))
    .retrieve()
    .bodyToMono(VetResource.class);
  }
}

Lässt sich der Wert eines Feldes, das nicht in einem Root-Typ definiert ist, von Spring GraphQL nicht per Reflection ermitteln, bedarf es für dieses Feld ebenfalls einer Handler-Funktion. Dazu sind das Schema erneut zu erweitern und der Visit-Typ um das Feld treatingVet zu ergänzen, das aussagt, welcher Tierarzt die Untersuchung durchgeführt hat. Ein treatingVet-Feld gibt es allerdings an der Vet-Entity nicht, die über den Pfad Owner.Pet.Visit referenziert wird, sodass hierfür eine eigene Handler-Funktion implementiert werden muss:

Listing: Erweiterung der API um den Vet

type Vet {
  id: Int!
  firstName: String!
  lastName: String!
  specialties: [String!]!
}

type Visit {
  # ...Felder wie oben gesehen...
  treatingVet: Vet
}

Handler-Funktionen für Felder an Nicht-Root-Typen sind mit @SchemaMapping annotiert. Dieser Annotation wird der Typname übergeben, auf den sich das Schema-Mapping bezieht. Der Methoden-Name entspricht dem Feld-Namen aus dem GraphQL-Schema. Spring GraphQL übergibt der Methode das "Source"-Objekt, also jenes, auf dem der Wert des Feldes ermittelt werden soll. Im Beispiel ist das eine Instanz der Visit-Klasse. Zur Erinnerung: GraphQL Java ermittelt die Liste der Owner über die QueryMapping-Funktion, navigiert dann per Reflection über die Pets, auf die Visits, ruft für jedes Visit-Objekt die neue Handler-Funktion auf und übergibt das jeweilige Visit-Objekt als Methoden-Parameter. Da auch diese Handler-Funktion ein reaktives Mono-Objekt zurückgibt, werden mehrere dieser Handler-Funktionen parallel ausgeführt, wenn der Tierarzt für mehr als einen Visit ermittelt werden soll:

Listing: Daten aus einem Microservice ermitteln

@Controller
public class VisitController {
  private WebClient webClient;

  public OwnerController() {
    this.webClient = WebClient.builder()
     .baseUrl(". . .")
     .build();
  }

  @SchemaMapping(typeName = "Visit")
  public Mono<VetResource> treatingVet(Visit visit) {
    return webClient.get()
      .uri(b -> b
        .path("/vets/{id}")
        .build(visit.getVetId()))
    .retrieve()
    .bodyToMono(VetResource.class);
  }
}