1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133
|
---
layout: guide
search: true
section: Dataloader
title: Overview
desc: Getting started with the Fiber-based Dataloader
index: 0
---
GraphQL-Ruby 1.12+ includes {{ "GraphQL::Dataloader" | api_doc }}, a module for managing efficient database access in a way that's transparent to application code, backed by Ruby's `Fiber` concurrency primitive. It also {% internal_link "supports Ruby 3's non-blocking fibers", "/dataloader/nonblocking" %}.
`GraphQL::Dataloader` is inspired by [`@bessey`'s proof-of-concept](https://github.com/bessey/graphql-fiber-test/tree/no-gem-changes) and [shopify/graphql-batch](https://github.com/shopify/graphql-batch).
## Batch Loading
`GraphQL::Dataloader` facilitates a two-stage approach to fetching data from external sources (like databases or APIs):
- First, GraphQL fields register their data requirements (eg, object IDs or query parameters)
- Then, after as many requirements have been gathered as possible, `GraphQL::Dataloader` initiates _actual_ fetches to external services
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:
- GraphQL execution is run inside a Fiber.
- When that Fiber returns, if the Fiber was paused to wait for data, then GraphQL execution resumes with the _next_ (sibling) GraphQL field inside a new Fiber.
- That cycle continues until no further sibling fields are available and all known Fibers are paused.
- `GraphQL::Dataloader` takes the first paused Fiber and resumes it, causing the `GraphQL::Dataloader::Source` to execute its `#fetch(...)` call. That Fiber continues execution as far as it can.
- Likewise, paused Fibers are resumed, causing GraphQL execution to continue, until all paused Fibers are evaluated completely.
Whenever `GraphQL::Dataloader` creates a new `Fiber`, it copies each pair from `Thread.current[...]` and reassigns them inside the new `Fiber`.
See {% internal_link "Non-Blocking", "/dataloader/nonblocking" %} for information about using Ruby 3's non-blocking Fibers.
## Getting Started
To install {{ "GraphQL::Dataloader" | api_doc }}, add it to your schema with `use ...`, for example:
```ruby
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(...)`:
```ruby
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(...)`:
```ruby
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:
```ruby
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:
```ruby
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:
```ruby
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 {% internal_link "Sources guide", "/dataloader/sources" %}.
|