[Spring Boot] Batching GraphQL Queries with @BatchMapping

Previously, I wrote an article covering how to create and test a Spring Boot app with GraphQL. At that time, it used an experimental graphql-spring-boot-starter dependency but now it is officially released as spring-boot-starter-graphql.

While I was at a webinar, Korea Spring Meetup with Josh Long, he introduced @BatchMapping annotation which can help solving the n+1 problem. Let’s find out what it is about.

Scenario

There are 3 different movies (movieInfos) and there are a total of 100 reviews. Each review has a movieInfoId which can be mapped to one of the three movies (1 movieInfo : n reviews relationship).

So the main goal is to serve a list of movieInfo with reviews.

Implementation

First of all, here is the project setup. I used spring-cloud-starter-gateway because I was testing a couple of things about gateway. It is not necessary to use this dependency. If you are taking out the gateway dependency, make sure it has spring-boot-starter-webflux dependency.

Note that spring boot version has to be newer than or equal to 2.7.0 because spring-boot-starter-graphql is available from 2.7.0 and onwards.

Secondly, we need to define GraphQL schema. Make sure to add this schema.graphqls file under src/main/resources/graphql.

The schema.graphqls file looks like this:

Lastly, here are the application properties. graphiql was enabled to make testing easier.

First Approach

So here is the first approach.

This works perfectly but there will be n+1 calls.

  1. fetch movieInfos (returns 3 movieInfos)
  2. fetch reviews for movieInfoId 1
  3. fetch reviews for movieInfoId 2
  4. fetch reviews for movieInfoId 3

From this point, reviews(..) method will change and the rest of the code will stay the same.

Second Approach

According to the GraphQL Java documentation, dataloader can help with batching requests and caching per request.

Using java-dataloader will help you to make this a more efficient process by both caching and batching requests for that graph of data items. If dataloader has seen a data item before, it will have cached the value and will return it without having to ask for it again.

And the @BatchMapping documentation describes how it registers a batch loader and returns from dataLoader.load(..).

The annotated method is registered as a batch loading function…

These clues gave strong evidences that @BatchMapping annotation was indeed implemented to solve the n+1 issue.

However, it wasn’t clear about what return types are required or supported at the beginning. According to the documentation,

In addition to returning Mono<Map<K,T>>, an @BatchMapping method can also return Flux<T>. However, in that case the returned sequence of values must match the number and order of the input keys.

So here was my initial trial:

However, it was not very successful because the movieInfoId of the movieInfo and the reviews do not match. This can be solved using flatMapSequential instead of flatMap.

However, this approach seemed identical to the first approach. While I was watching this tutorial, this tutorial demonstrates the exact same case where the order of data gets mixed up due to the asynchronous nature. To solve the problem, the tutorial shows the difference between registerBatchLoader() and registerMappedBatchLoader() and how registerMappedBatchLoader() solves this issue.

While looking at the implementation of the spring-boot-starter-graphql, I was able to find out registerBatchLoader() or registerMappedBatchLoader() are called depending on the return type of the method.

AnnotatedControllerConfigurer.java

Because I used Flux<T>, it used registerBatchLoader() which explains why movieInfo and reviews did not match appropriately. It also shows what return types are supported with @BatchMapping.

Third Approach

Lastly, I decided to use Mono<Map<K, V>> return type and also implemented a new endpoint in Movie-Review service to take a list of movieInfoIds as input. I came to realize that I already have a list of movieInfos inside @BatchMapping method which means I could combine 3 API calls into 1 API call by changing Movie-Review service implementation from findByMovieInfoId(Long movieInfoId) to findByMovieInfoIdIn(List<Long> movieInfoIds).

We saw earlier from GraphQL Java documentation, dataloader also supports caching (per request). This means if there was a data already fetched earlier during this same request, it will return from the cache instead making another HTTP call. In this example, each movieInfo or review is unique so there weren’t any benefits regarding caching.

Conclusion

This article covered how to make a simple Spring Boot app with GraphQL with spring-boot-starter-graphql. It also covered how to make batching requests in order to solve n+1 problem.

With @BatchMapping, I was able to get the same result as the first approach but with less HTTP calls.

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store