GraphQL is becoming an ever-more popular way to build modern APIs. In contrast to REST's more rigid route definitions, GraphQL allows you to describe the way your data is actually structured and the relationships between your different resources. The upshot of this is that you end up with a highly flexible API where it is possible to write queries to request any combination of data. The downside is, as the details of any query are unpredictable, it is more complicated to avoid N+1 queries, which can mean your API's increased utility comes at the cost of sluggish performance.
Facebook, the original creators of GraphQL, solve this problem in JavaScript with DataLoader, but how about in Ruby? There are a few gems that exist; for example Shopify's graphql-batch is a headline offering, and is lightweight and flexible. However, in this blog we're going to be using Universe's batch-loader, primarily because it is ready to go "out of the box" for ActiveRecord associations without having to define additional loader classes. If you'd prefer to use graphql-batch, see here and here for pointers.
N.B. It is relatively straightforward to add a simple GraphQL API to a Rails app using GraphQL Ruby. See their guides for detail on getting set up, the process will also add the GraphiQL editor to your app, which is awesome!
Imagine we have an app cataloguing museums and their exhibits, with a GraphQL API setup for querying them. Our basic museum type might look a little like this:
# app/graphql/types/museum_type.rb
module Types
class MuseumType < GraphQL::Schema::Object
field :name, String, null: false
field :exhibits, [ExhibitType], null: true
end
end
And our exhibit type. Let's keep this simple and only declare the association in a single direction for now:
# app/graphql/types/exhibit_type.rb
module Types
class ExhibitType < GraphQL::Schema::Object
field :name, String, null: false
end
end
And also our root query type (note for this example we are only declaring a root query for museums, not exhibits):
# app/graphql/types/query_type.rb
module Types
class QueryType < GraphQL::Schema::Object
field :museums, [MuseumType], null: true
def museums
Museum.all
end
end
end
Say we now want to run a query retrieving all of our museums and the name of each of their exhibits. Executing the following GraphQL query in our GraphiQL editor:
{
museums {
name
exhibits {
name
}
}
}
Will produce N+1 queries on the exhibits table (fig. 1). Boo! To modify our setup to use batch loading and zap that bug, add the following line to your Gemfile:
gem 'batch-loader'
And run bundle install, then make the following changes to the museum type:
# app/graphql/types/museum_type.rb
module Types
class MuseumType < GraphQL::Schema::Object
field :name, String, null: false
field :exhibits, [ExhibitType], null: true, resolve: -> (museum, args, ctx) do
BatchLoader::GraphQL.for(museum.id).batch(default_value: []) do |museum_ids, loader|
Exhibit.where(museum_id: museum_ids).each do |exhibit|
loader.call(exhibit.museum_id) { |memo| memo << exhibit }
end
end
end
end
end
BatchLoader is evaluated lazily, which means rather than the exhibits being loaded from the database instantly for each museum, the necessary museum IDs are stored and then executed in a single call to Exhibit.where, eliminating the N+1. BatchLoader will also cache the result of this query, so subsequent requests will be even faster as the database won't get hit at all. You can keep track of the caching behaviour by providing a key: option to .batch, and can clear the cache manually if desired by calling BatchLoader::Executor.clear_current.
Finally, in our schema we need to make the following change:
# app/graphql/app_schema.rb
class AppSchema < GraphQL::Schema
query(Types::QueryType)
# enable batch loading
use BatchLoader::GraphQL
end
Run the same query again:
{
museums {
name
exhibits {
name
}
}
}
Yay! No N+1 queries (fig. 2) and a nice, fast API response. As an additional exercise, try implementing a root :exhibits query type and batching in the opposite direction, such that you would be able to run the query:
{
exhibits {
name
museum {
name
}
}
}
And avoid the dreaded N+1. The BatchLoader block will be slightly simpler in this case, but the principles are the same. Enjoy!