Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to resolve graphql n+1 issue with graphql-jpa java?

Tags:

graphql-java

I am facing the graphql n+1 issue in one of my applications where I am using graphql with restful webservice in java. I am using schemagen-graphql with spring-data-jpa for connecting to my oracle db. I saw so many posts but most of the answer were graphql-node.js implementation. Anything related to java will be good.

like image 836
shaks Avatar asked Dec 10 '22 08:12

shaks


1 Answers

I don't know much about schemagen-graphql and whether it matters here at all but, in general, there's currently 3 options with graphql-java:

  1. Prefetch what you need when resolving the parent field (i.e. in the parent field's DataFetcher). You can look ahead to see exactly which sub-fields are requested by examining the DataFetchingFieldSelectionSet. If the prefetched data doesn't naturally fit into the result object, you can store it in the LocalContext and make use of it in when resolving the child field value. This approach is nicely explained in this blog post.
  2. Use either the deprecated (if you use the classes under graphql.execution) or the experimental (if use the classes under graphql.execution.nextgen) approach of annotating your data fetcher with @Batched and using BatchedExecutionStrategy.
    This option is simple to understand and use, but ties you to BatchedExecutionStrategy (the legacy version doesn't fully respect the specification when it comes to handling nulls).
    It is sadly poorly documented, but it boils down to this:
    1. Annotate the DataFetcher#get method with @Batched
    2. In that data fetcher, DataFetchingEnvironment#getSource will always return a list of source objects (instead of one).
    3. Such a data fetcher must always return a list of results (of the same length as the list of sources). This allows for a simple way to batch-load objects. An obvious example is loading many rows from a relational DB at once:
List<Article> articles = DataFetchingEnvironment.getSource(); //source is a list
List<Long> authorIds = articles.stream.map(article -> article.getAuthodId()).collect(Collectors.toList());

//fetch all the authors in one go    
SELECT * FROM Author WHERE author_id IN (authorIds)
  1. Use DataLoader to fetch your data instead of fetching it directly. The idea is very similar to the original data-loader JavaScript library. Works only with the default AsyncExecutionStrategy and only for queries (so don't expect it to work for mutations and subscriptions).

You can provide an instance of DataLoaderRegistry in ExecutionInput, and you normally want to re-create the data loaders and the registry on each request (batch loaders can be shared if they're stateless):

DataLoaderRegistry loaders = ...; //initialize your loaders, usually per request

graphQL.execute(ExecutionInput.newExecutionInput()
         .query(operation)
         .dataLoaderRegistry(loaders) //add the registry to input
         .build()); 

Then in your DataFetcher you can always get to the DataLoader you need via DataFetchingEnvironment#getDataLoader(String dataLoaderName):

return env.getDataLoader("authors").load(article.getAuthorId());

As a sidenote, you may want to check out my own library for generating the GraphQL API from Java, GraphQL-SPQR. Here's a minimal demonstration of using @Batched with it.

As for the DataLoader, the logic is the same as above, and you get to the DataLoaderRegistry via @GraphQLEnvironment annotation:

@GrapgQLQuery
public CompletableFuture<Author> author(@GraphQLContext Article article, @GraphQLEnvironment ResolutionEnvironment env) {
    return env.dataFetchingEnvironment.getDataLoader("authors").load(article.getAuthorId())
}

I'll likely also add a specialized annotation like @DataLoader("authors") for injecting the loaders directly.

like image 73
kaqqao Avatar answered Feb 04 '23 06:02

kaqqao