[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.
- fetch movieInfos (returns 3 movieInfos)
- fetch reviews for movieInfoId 1
- fetch reviews for movieInfoId 2
- 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. Ifdataloader
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.
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.
Reference: