File: lazy_execution.md

package info (click to toggle)
ruby-graphql 2.2.17-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 9,584 kB
  • sloc: ruby: 67,505; ansic: 1,753; yacc: 831; javascript: 331; makefile: 6
file content (96 lines) | stat: -rw-r--r-- 3,168 bytes parent folder | download
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
---
layout: guide
doc_stub: false
search: true
title: Lazy Execution
section: Schema
desc: Resolvers can return "unfinished" results that are deferred for batch resolution.
index: 4
---

With lazy execution, you can optimize access to external services (such as databases) by making batched calls. Building a lazy loader has three steps:

- Define a lazy-loading class with _one_ method for loading & returning a value
- Connect it to your schema with {{ "GraphQL::Schema#lazy_resolve" | api_doc }}
- In `resolve` methods, return instances of the lazy-loading class

## Example: Batched Find

Here's a way to find many objects by ID using one database call, preventing N+1 queries.

1. Lazy-loading class which finds models by ID.

```ruby
class LazyFindPerson
  def initialize(query_ctx, person_id)
    @person_id = person_id
    # Initialize the loading state for this query,
    # or get the previously-initiated state
    @lazy_state = query_ctx[:lazy_find_person] ||= {
      pending_ids: Set.new,
      loaded_ids: {},
    }
    # Register this ID to be loaded later:
    @lazy_state[:pending_ids] << person_id
  end

  # Return the loaded record, hitting the database if needed
  def person
    # Check if the record was already loaded:
    loaded_record = @lazy_state[:loaded_ids][@person_id]
    if loaded_record
      # The pending IDs were already loaded,
      # so return the result of that previous load
      loaded_record
    else
      # The record hasn't been loaded yet, so
      # hit the database with all pending IDs
      pending_ids = @lazy_state[:pending_ids].to_a
      people = Person.where(id: pending_ids)
      people.each { |person| @lazy_state[:loaded_ids][person.id] = person }
      @lazy_state[:pending_ids].clear
      # Now, get the matching person from the loaded result:
      @lazy_state[:loaded_ids][@person_id]
    end
  end
```

2. Connect the lazy resolve method

```ruby
class MySchema < GraphQL::Schema
  # ...
  lazy_resolve(LazyFindPerson, :person)
end
```

3. Return lazy objects from `resolve`

```ruby
field :author, PersonType

def author
  LazyFindPerson.new(context, object.author_id)
end
```

Now, calls to `author` will use batched database access. For example, this query:

```graphql
{
  p1: post(id: 1) { author { name } }
  p2: post(id: 2) { author { name } }
  p3: post(id: 3) { author { name } }
}
```

Will only make one query to load the `author` values.

## Gems for batching

The example above is simple and has some shortcomings. Consider the following gems for a robust solution to batched resolution:

* {{ "GraphQL::Dataloader" | api_doc }} is a built-in, Fiber-based approach to batching. See the {% internal_link "Dataloader guide", "/dataloader/overview" %} for more information.
* [`graphql-batch`](https://github.com/shopify/graphql-batch) provides a powerful, flexible toolkit for lazy resolution with GraphQL.
* [`dataloader`](https://github.com/sheerun/dataloader) is more general promise-based utility for batching queries within the same thread.
* [`batch-loader`](https://github.com/exAspArk/batch-loader) works with any Ruby code including GraphQL, no extra dependencies or primitives.