An example of how to solve the N+1 problem for GraphQL using graphql-dotnet
This exmaple uses .net core. You can get it and learn more about it from https://www.microsoft.com/net/core
I use entity framework + sqlite to create a simple database to run queries.
You will need to run the commands below if you are running the code for the first time.
dotnet restore
dotnet ef migrations add InitialSetup
dotnet ef database update
If you want to recreate the database, run the following commands to drop the db first, then you can run the previous commands to create it again.
dotnet ef database drop
dotnet ef migrations remove
In the original resolving process, siblings are resolved individually. Which means that even though all of the children of some given siblings are stored in the same table, we will call the database once for each sibling, hitting the same table over and over. This is inefficient for most databases such as mssql, or sqlite.
This sample code tries to improve the performance of resolving graphql by only calling the database once per child property. For example, assuming, in the StarWars database, we have 3 humans and each of them has 2 friends.
When we resolve this graphql
query HumansQuery {
humans {
name
friends {
name
}
}
}
We only want to call the database once for resolving friends
, rather than
calling the database 3 times.
To achieve this goal, we need two pieces of information when we resolve a child property of a sibling. Firstly, we need to have access to all of the siblings so that we can compose a query to the database containing all the parent ids (that is, the ids of the siblings). Secondly, we need a way to tell if the query for the child property has already been called to ensure we only call the database once.
In this code, we have two core classes, namely:
GraphNode<T>
and NodeCollection<T>
.
GraphNode<T>
has a property called Collection
which contains all of the
siblings of the Node.
NodeCollection<T>
has a loader function, and a private field _nodes
.
This class will only call the loader function once to fill the _nodes
field
and will not call the loader again if _nodes
has already been initialized.
NodeCollection<T>
also has a private dictionary called _relations
.
This dictionary stores all of the resolved child properties. When resolving a
child, we first check the dictionary to see if the child has already been resolved.
If it has, we grab the stored result rather than calling the database again.
With help from these two classes, we achieve the goal of only calling the database once per child property.
Now, let's have a look at a real example.
Here's how the resolve function of the friends
field is implemented for HumanType
:
public class HumanType : ObjectGraphType<GraphNode<Human>>
{
// This indexer will generate a map from each human to their friends.
// It will perform better than search for droids in for loop.
private static NodeCollectionIndexer<Droid, int> _friendsIndexer =
new NodeCollectionIndexer<Droid, int>(
nc => nc.
Select(n => n.Node).
SelectMany(h => h.Friends).
GroupBy(h => h.HumanId).
ToDictionary(g => g.Key, g => g.Select(f => new GraphNode<Droid>(f.Droid, nc)).ToArray()));
public HumanType(StarWarsContext db)
{
Name = "Human";
Field(x => x.Node.HumanId).
Name("id").
Description("The id of the human.");
Field(x => x.Node.Name).
Name("name").
Description("The name of the human.");
Field(x => x.Node.HomePlanet).
Name("homePlanet").
Description("The home planet of the human.");
Field<ListGraphType<CharacterInterface>>(
"friends",
resolve: context =>
{
// All of the humans we are resolving in the query.
var collection = context.GetNodeCollection();
// Fetch all of the friends for every human we are resolving (only once).
// GetOrAddRelation will first check if there is a stored result for the indexer
// if the result exists, it will immediately return the stored result
// otherwise, it will create a new NodeCollection using the given loader function
var indexedCollection = collection.GetOrAddRelation(
_friendsIndexer,
() =>
{
var humanIds = collection.Select(n => n.Node.HumanId).ToArray();
Console.WriteLine("Loading all friends for humans");
var droids = db.Droids.
Where(d => d.Friends.Any(f => humanIds.Contains(f.HumanId))).
Include(d => d.Friends).
ToList();
return new NodeCollection<Droid>(droids);
});
// Return the friends of the human currently being resolved.
var human = context.GetGraphNode();
return indexedCollection.GetManyByKey(human.HumanId);
}
);
Interface<CharacterInterface>();
IsTypeOf = value => value is GraphNode<Human>;
}
}