Solving Complex N+1 Queries in GraphQL Ruby with BatchLoader

I’m doing some performance and scalability updates on an application written in a mix of Ruby on Rails and React with Typescript. So far, we’ve added a GraphQL endpoint to our existing Rails app in order to improve the performance of several dashboard-style pages.

We brought in the excellent GraphQL Ruby gem to define and serve up our GraphQL schema. This gem comes with a neat generator tool that creates GraphQL types from your existing Rails model classes. Of course, as we built out our schema, we quickly ran into the N+1 query problem that almost always comes into play when building a GraphQL API.

To address the N+1 problem, we needed some form of batch loading or lazy loading that could integrate easily with Ruby GraphQL. Initially, we tried out GraphQL Batch. True to Ruby on Rails form, this gem patched into the GraphQL Ruby DSL seamlessly and allowed some simple cases to be dealt with in one line of code. However, we quickly ran into a wall of complications as our queries became more nuanced and complex. Checking permissions; joining across has_many, belongs_to, and has_many:through associations; or doing things like counting aggregates all required custom handler classes.

Then we tried out BatchLoader. Its website lists a few reasons it stands out from other batch loading alternatives, and one reason caught our eye:

BatchLoader doesn’t force you to share batching through variables or custom defined classes, just pass a block to the batch method.

This block-centric API let us easily build out correctly batched fields, even if fields were based on more complex relationships.

Consider a set of model classes for a hypothetical school system:

class District < ApplicationRecord
  has_many :buildings

class Building < ApplicationRecord
  belongs_to :district
  has_many :rooms
  has_many :classes,
    through: :rooms,
    source: :room
  has_many :employees

class Room < ApplicationRecord
  belongs_to :building
  has_many :classes

class Class < ApplicationRecord
  belongs_to :room

class Employee < ApplicationRecord
  belongs_to :building

A possible query might look something like this:

query BuildingInfoForDistrict {
  districtById(id: 12) {
    buildings {
      rooms {
      classes {
      teachers: employees(type: "teacher")
      aministrators: employees(type: "admin")

We want to find out some information about each building in district twelve:

  • How many rooms are there?
  • When is each room cleaned?
  • Who teaches each class?
  • Who are the teachers who work there?
  • Who are the administrators who work there?

Below are examples of how implement each field on BuildingType so that resolving this query requires a constant number of database queries.

Counting Associated Objects

def roomCount
  BatchLoader::GraphQL.for( 0) do |building_ids, loader|
    Room.where(building_id: building_ids)
      .each { |building_id, count|, count) }

Here, we let ActiveRecord and the database handle grouping and counting the number of rooms in each building. Once the query returns, loop through the results, and resolve each building ID with its associated count.

Has Many

def rooms
  BatchLoader::GraphQL.for( []) do |building_ids, loader|
    Room.where(building_id: building_ids)
      .each { |room| { |rooms| rooms << room } }

This example demonstrates batch loading a has_many relationship. It’s conceptually similar to the group and count example, but we need the actual rooms instead of the count. BatchLoader allows us to pass a block to the loader lambda, which allows us to add each room to an array associated with that room’s building ID.

Has Many Through

def classes
  BatchLoader::GraphQL.for( []) do |building_ids, loader|
    Room.where(building_id: building_ids)
      .each { |room|, room.classes) }

In this case, we need to determine all the classes owned by all the rooms in each building. The trouble is that classes are associated with buildings through rooms. This means that if we call into the Class model, we’ll have no way to associate our array of building IDs with classes. But how do we return a Class object from a query against the Room model?

We can actually use Rails’ built-in eager loading tool. By adding includes(:classes), we tell Rails to first get all the matching rooms, then load all the associated classes at once and keep them in memory. That way, when we loop over the resulting rooms, we don’t trigger another database query each time we access room.classes.

Batching Fields with Arguments

def employees(type: nil)
  BatchLoader::GraphQL.for( [], key: type) do |building_ids, loader, args|
    employee_type = args[:key]
    Employees.where(building_id: building_ids)
      .where(type: employee_type)
      .each { |employee|, employee) }

Finally, we have our employees field. This field accepts a parameter that lets us filter by what type of employee we’re interested in. How can we batch load these requests?

BatchLoader allows us to specify a key for the batch. In this case, we set our key to the requested employee type. Under the hood, BatchLoader partitions all of the requests whose keys match into the same batch. In the handler block, we can pull our employee type back out of arguments and resolve all the employees of the matching type for all of the building IDs in the batch.

This approach also handles arbitrary arguments. Simply bundle args up into a hash and set the batch key to the JSON dump of the hash. Then all requests with matching arguments will be batched together, and the args can be extracted with JSON.parse().

BatchLoader is a great tool for addressing the N+1 query problem in a GraphQL Ruby API. The simple, block-based API allows you to easily write custom field resolvers using normal ActiveRecord query strategies without cluttering your project up with lots of custom handler classes.