⚠ Experimental ⚠

This feature may get big changes in future releases. Check the changelog for update notes.

Overview

GraphQL-Ruby 1.12 includes GraphQL::Dataloader, a module for managing efficient database access in a way that’s transparent to application code, backed by Ruby’s Fiber concurrency primitive.

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:

Since all GraphQL execution is inside a Fiber, Thread.current[...] won’t be set. Those assignments are Fiber-local, so each new Fiber has an empty Thread.current. For GraphQL::Dataloader, use GraphQL-Ruby’s context object to provide “current” values to a GraphQL query. Inside #fetch(...) methods, those values can be re-assigned to Thread.current if application code requires it.

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, null: true do
  argument :handle, String, required: true
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, required: true
  argument :followed_handle, String, required: true
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, required: true
  argument :list, String, required: true, as: :list_name

  field :list, Types::UserList, null: true

  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, required: true, loads: Types::User

  field :followed, Types::User, null: true

  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.