Overview

GraphQL::Dataloader provides efficient, batched access to external services, backed by Ruby’s Fiber concurrency primitive. It has a per-query result cache and AsyncDataloader supports truly parallel execution out-of-the-box.

GraphQL::Dataloader is inspired by @bessey’s proof-of-concept and shopify/graphql-batch.

Batch Loading

GraphQL::Dataloader facilitates a two-stage approach to fetching data from external sources (like databases or APIs):

That cycle is repeated during execution: data requirements are gathered until no further GraphQL fields can be executed, then GraphQL::Dataloader triggers external calls based on those requirements and GraphQL execution resumes.

Fibers

GraphQL::Dataloader uses Ruby’s Fiber, a lightweight concurrency primitive which supports application-level scheduling within a Thread. By using Fiber, GraphQL::Dataloader can pause GraphQL execution when data is requested, then resume execution after the data is fetched.

At a high level, GraphQL::Dataloader’s usage of Fiber looks like this:

Whenever GraphQL::Dataloader creates a new Fiber, it copies each pair from Thread.current[...] and reassigns them inside the new Fiber.

AsyncDataloader, built on top of the async gem, supports parallel I/O operations (like network and database communication) via Ruby’s non-blocking Fiber.schedule API. Learn more →.

Getting Started

To install GraphQL::Dataloader, add it to your schema with use ..., for example:

class MySchema < GraphQL::Schema
  # ...
  use GraphQL::Dataloader
end

Then, inside your schema, you can request batch-loaded objects by their lookup key with dataloader.with(...).load(...):

field :user, Types::User do
  argument :handle, String
end

def user(handle:)
  dataloader.with(Sources::UserByHandle).load(handle)
end

Or, load several objects by passing an array of lookup keys to .load_all(...):

field :is_following, Boolean, null: false do
  argument :follower_handle, String
  argument :followed_handle, String
end

def is_following(follower_handle:, followed_handle:)
  follower, followed = dataloader
    .with(Sources::UserByHandle)
    .load_all([follower_handle, followed_handle])

  followed && follower && follower.follows?(followed)
end

To prepare requests from several sources, use .request(...), then call .load after all requests are registered:

class AddToList < GraphQL::Schema::Mutation
  argument :handle, String
  argument :list, String, as: :list_name

  field :list, Types::UserList

  def resolve(handle:, list_name:)
    # first, register the requests:
    user_request = dataloader.with(Sources::UserByHandle).request(handle)
    list_request = dataloader.with(Sources::ListByName, context[:viewer]).request(list_name)
    # then, use `.load` to wait for the external call and return the object:
    user = user_request.load
    list = list_request.load
    # Now, all objects are ready.
    list.add_user!(user)
    { list: list }
  end
end

loads: and object_from_id

dataloader is also available as context.dataloader, so you can use it to implement MySchema.object_from_id. For example:

class MySchema < GraphQL::Schema
  def self.object_from_id(id, ctx)
    model_class, database_id = IdDecoder.decode(id)
    ctx.dataloader.with(Sources::RecordById, model_class).load(database_id)
  end
end

Then, any arguments with loads: will use that method to fetch objects. For example:

class FollowUser < GraphQL::Schema::Mutation
  argument :follow_id, ID, loads: Types::User

  field :followed, Types::User

  def resolve(follow:)
    # `follow` was fetched using the Schema's `object_from_id` hook
    context[:viewer].follow!(follow)
    { followed: follow }
  end
end

Data Sources

To implement batch-loading data sources, see the Sources guide.

Parallelism

You can run I/O operations in parallel with GraphQL::Dataloader. There are two approaches: