Skip to content

Commit a511437

Browse files
committed
exec-next: Add dataload: shorthands
1 parent 6758b11 commit a511437

5 files changed

Lines changed: 122 additions & 21 deletions

File tree

guides/execution/next.md

Lines changed: 64 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -111,32 +111,75 @@ This is a high-performance option for when you need to do I/O to generate result
111111

112112
These fields use a _class method_ to map parent objects to field results, configured with `resolve_batch:`:
113113

114-
```ruby
115-
field :title, String, resolve_batch: :titles do
116-
argument :language, Types::Language, required: false, default_value: "EN"
117-
end
114+
```ruby
115+
field :title, String, resolve_batch: :titles do
116+
argument :language, Types::Language, required: false, default_value: "EN"
117+
end
118118

119-
def self.titles(objects, context, language:)
120-
# This is equivalent to plain `field :title, ...`, but for example:
121-
objects.map { |obj| obj.title(language:) }
122-
end
123-
```
119+
def self.titles(objects, context, language:)
120+
# This is equivalent to plain `field :title, ...`, but for example:
121+
objects.map { |obj| obj.title(language:) }
122+
end
123+
```
124124

125-
This is especially useful when batching Dataloader calls:
125+
This is especially useful when batching Dataloader calls:
126126

127-
```ruby
128-
class Types::Comment < BaseObject
129-
field :post, Types::Post, resolve_batch: :posts
127+
```ruby
128+
class Types::Comment < BaseObject
129+
field :author_rating, Integer, resolve_batch: true
130130

131-
# Use `.load_all(ids)` to fetch all in a single round-trip
132-
def self.posts(objects, context)
133-
# TODO: add a shorthand for this in GraphQL-Ruby
134-
context.dataloader
135-
.with(GraphQL::Dataloader::ActiveRecordSource)
136-
.load_all(objects.map(&:post_id))
137-
end
131+
def self.author_rating(objects, context)
132+
authors = context.dataload_all_records(objects, :author)
133+
context.dataload_all(Sources::AuthorRating, authors)
138134
end
139-
```
135+
end
136+
```
137+
138+
### Dataloader
139+
140+
`Execution::Next` supports field configuration shorthands for common dataloader usage. Under the hood, these make sure data fetching is batched and cached.
141+
142+
#### Sources
143+
144+
Use a custom dataloader source from your application:
145+
146+
```ruby
147+
class Types::CommentType
148+
# Equivalent to `dataload(Sources::CommentRating, object)`
149+
field :rating, Integer, dataload: Sources::CommentRating
150+
151+
# `using:`: A method to call to get a value to pass to dataloader
152+
# `by: [...]`: An array of arguments to pass on to dataloader
153+
#
154+
# Equivalent to `dataload(Sources::ReadingDuration, :comment, object.body)
155+
field :reading_duration, Integer, dataload: { with: Sources::ReadingDuration, using: :body, by: [:comment] }
156+
```
157+
158+
#### Rails Associations
159+
160+
Load ActiveRecord associations using {{ "GraphQL::Dataloader::ActiveRecordAssociationSource" | api_doc }}:
161+
162+
```ruby
163+
class Types::CommentType < Types::BaseObject
164+
# Equivalent to `dataload_association(:post)`
165+
field :post, Types::Post, dataload: { association: true }
166+
# Equivalent to `dataload_association(:user)
167+
field :author, Types::Post, dataload: { association: :user }
168+
end
169+
```
170+
171+
#### Rails Records
172+
173+
Load ActiveRecord associations using {{ "GraphQL::Dataloader::ActiveRecordSource" | api_doc }}.
174+
175+
```ruby
176+
class Types::SearchResult < Types::BaseObject
177+
# Equivalent to `dataload_record(Post, object.post_id)`
178+
field :post, Types::Post, dataload: { model: Post, using: :post_id }
179+
# Equivalent to `dataload_record(User, object.created_by_handle, find_by: :handle)`
180+
field :author, Types::User, dataload: { model: User, using: :created_by_handle, find_by: :handle }
181+
end
182+
```
140183

141184
### Legacy instance methods
142185

lib/graphql/execution/next/field_resolve_step.rb

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -644,6 +644,27 @@ def resolve_batch(objects, context, args_hash)
644644
end
645645
when :dig
646646
objects.map { |o| o.dig(*@field_definition.execution_next_mode_key) }
647+
when :dataload
648+
if (k = @field_definition.execution_next_mode_key).is_a?(Class)
649+
context.dataload_all(k, objects)
650+
elsif (source_class = k[:with])
651+
if (batch_args = k[:by])
652+
context.dataload_all(source_class, *batch_args, objects)
653+
else
654+
context.dataload_all(source_class, objects)
655+
end
656+
elsif (model = k[:model])
657+
value_method = k[:using]
658+
values = objects.map(&value_method)
659+
context.dataload_all_records(model, values, find_by: k[:find_by])
660+
elsif (assoc = k[:association])
661+
if assoc == true
662+
assoc = @field_definition.original_name
663+
end
664+
context.dataload_all_associations(objects, assoc, scope: k[:scope])
665+
else
666+
raise ArgumentError, "Unexpected `dataload: ...` configuration: #{k.inspect}"
667+
end
647668
when :resolver_class
648669
results = Array.new(objects.size, nil)
649670
ps = @pending_steps ||= []

lib/graphql/schema/member/has_dataloader.rb

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,16 @@ def dataload_record(model, find_by_value, find_by: nil)
5656
source.load(find_by_value)
5757
end
5858

59+
# @see dataload_record Like `dataload_record`, but accepts an Array of `find_by_values`
60+
def dataload_all_records(model, find_by_values, find_by: nil)
61+
source = if find_by
62+
dataloader.with(Dataloader::ActiveRecordSource, model, find_by: find_by)
63+
else
64+
dataloader.with(Dataloader::ActiveRecordSource, model)
65+
end
66+
source.load_all(find_by_values)
67+
end
68+
5969
# Look up an associated record using a Rails association (via {Dataloader::ActiveRecordAssociationSource})
6070
# @param association_name [Symbol] A `belongs_to` or `has_one` association. (If a `has_many` association is named here, it will be selected without pagination.)
6171
# @param record [ActiveRecord::Base] The object that the association belongs to.
@@ -73,6 +83,16 @@ def dataload_association(record = object, association_name, scope: nil)
7383
end
7484
source.load(record)
7585
end
86+
87+
# @see dataload_association Like `dataload_assocation` but accepts an Array of records (required param)
88+
def dataload_all_associations(records, association_name, scope: nil)
89+
source = if scope
90+
dataloader.with(Dataloader::ActiveRecordAssociationSource, association_name, scope)
91+
else
92+
dataloader.with(Dataloader::ActiveRecordAssociationSource, association_name)
93+
end
94+
source.load_all(records)
95+
end
7696
end
7797
end
7898
end

spec/graphql/schema/member/has_dataloader_spec.rb

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,12 @@ def fetch(keys)
2626
assert_equal 4, example.dataload_record(Album, "Homey", find_by: :name).id
2727
end
2828

29+
it_dataloads "loads many records with dataload_all_records" do |d|
30+
example = DataloaderExample.new(d)
31+
assert_equal ["Homey", "Mit Peck"], example.dataload_all_records(Album, [4, 1]).map(&:name)
32+
assert_equal [4, 1], example.dataload_all_records(Album, ["Homey", "Mit Peck"], find_by: :name).map(&:id)
33+
end
34+
2935
it_dataloads "loads association with dataload_association" do |d|
3036
album1 = Album.find(1)
3137
example = DataloaderExample.new(d, album1)
@@ -37,6 +43,17 @@ def fetch(keys)
3743
assert_nil example.dataload_association(album, :band, scope: Band.country)
3844
end
3945

46+
it_dataloads "loads association on many objects with dataload_all_associations" do |d|
47+
album1 = Album.find(1)
48+
album4 = Album.find(4)
49+
example = DataloaderExample.new(d, album1)
50+
51+
assert_equal ["Vulfpeck", "Chon"], example.dataload_all_associations([album1, album4], :band).map(&:name)
52+
album1.reload
53+
album4.reload
54+
assert_equal [nil, nil], example.dataload_all_associations([album1, album4], :band, scope: Band.country)
55+
end
56+
4057
it_dataloads "calls any source with dataload..." do |d|
4158
example = DataloaderExample.new(d)
4259
d.with(PlusSource).request(2)

0 commit comments

Comments
 (0)