Solving GraphQL N+1 problem in fastify with loaders and conditional queries
What is 1+N, err, N+1 ?
To understand this, let’s give an example.
Lets start by defining our graphql schema in a schema.graphql file
1 | type Song { |
Normally we would have a songs table and a genres table in a relational database, say PostgreSQL:
Songs table:
1 | CREATE TABLE Songs ( |
Genres table
1 | CREATE TABLE Genres ( |
Here, a genre can be in many songs, we’re linking the two tables by having a foreign key (genreid ) in our songs table that references the genreid column in the genres table.
1 | ALTER TABLE songs |
Now let’s define a query to fetch our songs and their genres from our server.
1 | query { |
We define our resolvers in resolvers.js file:
1 | const resolvers = { |
If we ran our query and inspect the logs we see:
1 | {"msg":"getting all songs"} |
What’s happening?
We are fetching all songs from our database and for each song we are also making
a database request to get the genre, hence the “N + 1” problem.
That’s really not efficient, especially with nested queries in many to many relations. A GraphQL API should fetch data efficiently as possible.
Solutions
Solution 1: Using a join
One of the solution would be to perform a SQL INNER JOIN.
Now our reslovers will look like this:
1 | const resolvers = { |
In our logs we’ll see this:
1 | {"msg":"getting all songs and genres"} |
Great! we have eliminated the extra SQL queries from our previous resolvers.
Have we really?
The problem with this:
Let’s say our API user now creates a query to fetch the songid and songname only, no genre.
1 | query { |
If you could check your database logs, you’ll notice that we still have the join which is really unnecessary in this case.
The same SQL query will be executed even though we don’t need the genre. That’s not very efficient right?
Solution 2: Conditional queries
What if we could be able to look into the query fields, see which fields our users have requested and create conditional SQL queries that returns only the data they requested.
That sounds great, but how do we do that?
The GraphQL info
object
Our resolver function takes four arguments: parent
, args
, context
and info
. The info
object contains, well, information on the incoming GraphQl query. What we are interested in are the fields requested in the info.
We could parse the info
ourselves but there are some libraries we could use for that.
It parses the info
object and returns the fields that are being requested by our API user enabling us to optimise our resolvers.
1 | npm i graphql-parse-resolve-info |
Usage
In our reslovers.js
file require the module
1 | const {parseResolveInfo,simplifyParsedResolveInfoFragmentWithType} = require('graphql-parse-resolve-info'); |
We use the first function to parse the info
object and the second function as a helper to obtain the fields in our returnType, in our case Song
. From these, we can create conditional SQL queries.
By using the hasOwnproperty()
method, we can check whether our Song
object has the field we need and execute the SQL queries conditionally.
Now our resolvers will look like this:
1 | const resolvers = { |
The problem with this:
This is a good solution for simple query types, in our case, we only check whether the API user includes the genre
in the query fields and perform the conditional SQL queries.
However, with complex query types, our resolvers could get really messy and verbose.
Solution 3: Loaders
From mercurius documentation:
Each defined loader will register a resolver that coalesces each of the request and combines them into a single, bulk query.
Loaders enable us to write resolvers that batch requests.
Mercurius - the graphql adapter for fastify - comes with great set of features including automatic loaders integration to solve 1 + N queries.
We just need to define our loaders and add them in the options object where we register the mercurius plugin.
In theserver.js
we have:
1 | fastify.register(mercurius,{ |
Refactor our resolver and add our loader
1 | const resolvers = { |
1 | const loaders = { |
We have created a resolver for the genre
in our Song
object type. The loader receives two arguments, queries
and context
. queries
is an array of objects in our case this:
1 | [ |
We map this object to obtain all the genreids
, and perform a SQL batch query.
1 | let genreids = queries.map(({ obj }) => obj.genreid) |
We then return the result ordered by the genreids.
1 | return genreids.map(genreid => { |
Our resolvers can now query our database efficiently.