Skip to content

Commit ba7242c

Browse files
authored
Merge pull request #5571 from rmosolgo/exec-next-legacy-instance-method
Exec-next: legacy instance methods
2 parents b79712c + ee1b36c commit ba7242c

28 files changed

Lines changed: 415 additions & 370 deletions

benchmark/run.rb

Lines changed: 34 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# frozen_string_literal: true
22
require "graphql"
33
ADD_WARDEN = false
4+
TESTING_EXEC_NEXT = !!ENV["GRAPHQL_FUTURE"]
45
require "jazz"
56
require "benchmark/ips"
67
require "stackprof"
@@ -265,32 +266,36 @@ def self.profile_large_analysis
265266

266267
# Adapted from https://github.com/rmosolgo/graphql-ruby/issues/861
267268
def self.profile_large_result
268-
require "graphql/execution/batching"
269-
GraphQL::Schema::Field.prepend(GraphQL::Execution::Batching::FieldCompatibility)
270269
schema = ProfileLargeResult::Schema
271270
schema.use(GraphQL::Dataloader)
272271
document = ProfileLargeResult::ALL_FIELDS
273-
# Benchmark.ips do |x|
274-
# x.config(time: 10)
275-
# x.report("Querying for #{ProfileLargeResult::DATA.size} objects") {
276-
# schema.execute(document: document)
277-
# }
278-
# end
279-
schema.execute_batching(document: document)
272+
Benchmark.ips do |x|
273+
x.config(time: 5)
274+
x.report("exec #{ProfileLargeResult::DATA.size} objects") {
275+
schema.execute(document: document)
276+
}
277+
x.report("exec_next #{ProfileLargeResult::DATA.size} objects") {
278+
schema.execute_next(document: document)
279+
}
280+
end
281+
r1 = schema.execute_next(document: document)
282+
r2 = schema.execute(document: document)
283+
if r1 != r2
284+
raise "Result mismatch:\n\n#{r1.inspect}\n\n#{r2.inspect}"
285+
end
286+
287+
exec_method = TESTING_EXEC_NEXT ? :execute_next : :execute
280288
result = StackProf.run(mode: :wall, interval: 1) do
281-
schema.execute_batching(document: document)
282-
# schema.execute(document: document)
289+
schema.public_send(exec_method, document: document)
283290
end
284291
StackProf::Report.new(result).print_text
285292

286293
StackProf.run(mode: :wall, interval: 1, out: "tmp/stackprof.dump") do
287-
schema.execute_batching(document: document)
288-
# schema.execute(document: document)
294+
schema.public_send(exec_method, document: document)
289295
end
290296

291297
report = MemoryProfiler.report do
292-
# schema.execute(document: document)
293-
schema.execute_batching(document: document)
298+
schema.public_send(exec_method, document: document)
294299
end
295300

296301
report.pretty_print
@@ -389,14 +394,14 @@ def self.eager_or_proc(value)
389394

390395
module Bar
391396
include GraphQL::Schema::Interface
392-
field :string_array, [String], null: false
397+
field :string_array, [String], null: false, hash_key: :string_array
393398
end
394399

395400
module Baz
396401
include GraphQL::Schema::Interface
397402
implements Bar
398-
field :int_array, [Integer], null: false
399-
field :boolean_array, [Boolean], null: false
403+
field :int_array, [Integer], null: false, hash_key: :int_array
404+
field :boolean_array, [Boolean], null: false, hash_key: :boolean_array
400405
end
401406

402407

@@ -405,53 +410,53 @@ class ExampleExtension < GraphQL::Schema::FieldExtension
405410

406411
class FooType < GraphQL::Schema::Object
407412
implements Baz
408-
field :id, ID, null: false, extensions: [ExampleExtension]
409-
field :int1, Integer, null: false, extensions: [ExampleExtension]
410-
field :int2, Integer, null: false, extensions: [ExampleExtension]
411-
field :string1, String, null: false do
413+
field :id, ID, null: false, extensions: [ExampleExtension], hash_key: :id
414+
field :int1, Integer, null: false, extensions: [ExampleExtension], hash_key: :int1
415+
field :int2, Integer, null: false, extensions: [ExampleExtension], hash_key: :int2
416+
field :string1, String, null: false, hash_key: :string1 do
412417
argument :arg1, String, required: false
413418
argument :arg2, String, required: false
414419
argument :arg3, String, required: false
415420
argument :arg4, String, required: false
416421
end
417422

418-
field :string2, String, null: false do
423+
field :string2, String, null: false, hash_key: :string2 do
419424
argument :arg1, String, required: false
420425
argument :arg2, String, required: false
421426
argument :arg3, String, required: false
422427
argument :arg4, String, required: false
423428
end
424429

425-
field :boolean1, Boolean, null: false do
430+
field :boolean1, Boolean, null: false, hash_key: :boolean1 do
426431
argument :arg1, String, required: false
427432
argument :arg2, String, required: false
428433
argument :arg3, String, required: false
429434
argument :arg4, String, required: false
430435
end
431-
field :boolean2, Boolean, null: false do
436+
field :boolean2, Boolean, null: false, hash_key: :boolean2 do
432437
argument :arg1, String, required: false
433438
argument :arg2, String, required: false
434439
argument :arg3, String, required: false
435440
argument :arg4, String, required: false
436441
end
437442

438-
field :foos, [FooType], null: false, description: "Return a list of Foo objects" do
443+
field :foos, [FooType], null: false, description: "Return a list of Foo objects", resolve_legacy_instance_method: true do
439444
argument :first, Integer, default_value: DATA_SIZE
440445
end
441446

442447
def foos(first:)
443448
DATA.first(first)
444449
end
445450

446-
field :foo, FooType
451+
field :foo, FooType, resolve_legacy_instance_method: true
447452
def foo
448453
DATA.sample
449454
end
450455
end
451456

452457
class QueryType < GraphQL::Schema::Object
453458
description "Query root of the system"
454-
field :foos, [FooType], null: false, description: "Return a list of Foo objects" do
459+
field :foos, [FooType], null: false, description: "Return a list of Foo objects", resolve_legacy_instance_method: true do
455460
argument :first, Integer, default_value: DATA_SIZE
456461
end
457462
def foos(first:)
@@ -461,6 +466,7 @@ def foos(first:)
461466

462467
class Schema < GraphQL::Schema
463468
query QueryType
469+
use GraphQL::Execution::Next
464470
# use GraphQL::Dataloader
465471
if !ENV["EAGER"]
466472
lazy_resolve Proc, :call

guides/execution/migration.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,20 @@ render json: result.to_h
145145

146146
Performance improvements in batching execution come at the cost of removing support for many "nice-to-have" features in GraphQL-Ruby by default. Those features are addressed here.
147147

148+
### Implicit Field Resolution
149+
150+
The _default_, _implicit_ field resolution behavior has changed. Previously, when a field didn't have a specified method or hash key, GraphQL-Ruby would try a combination of `object.public_send(...)` and `object[...]` to resolve it. In `Execution::Next`, GraphQL-Ruby tries `object.public_send(field_sym)` unless another configuration is provided. This removes a lot of overhead from field execution.
151+
152+
Consider a field like this:
153+
154+
```ruby
155+
field :title, String
156+
```
157+
158+
Previously, GraphQL-Ruby would check `type_object.respond_to?(:title)`, `object.respond_to?(:title)`, `object.is_a?(Hash)`. `object.key?(:title)` and `object.key?("title")`.
159+
160+
Now, GraphQL-Ruby simply calls `object.title` and allows the `NoMethodError` to bubble up if one is raised.
161+
148162
### Query Analyzers, including complexity 🌕
149163

150164
Support is identical; this runs before execution using the exact same code.

guides/execution/next.md

Lines changed: 96 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -44,89 +44,125 @@ See {% internal_link "compatibility notes", "/execution/migration#compatibility-
4444

4545
## Field configurations
4646

47-
The new runtime engine supports several field resolution configurations out of the box:
47+
The new runtime engine supports several field resolution configurations out of the box.
4848

49-
- __Method calls__: fields that call `object.#{field_name}`. This is the default, and the method name can be overridden with `method: ...`:
49+
### Method calls (default, `method:`)
5050

51-
```ruby
52-
field :title, String # calls object.title
53-
field :title, String, method: :get_title_somehow # calls object.get_title_somehow
54-
```
55-
- __Hash keys__: fields that call `object[hash_key]`, configured with `hash_key: ...`.
51+
Fields that call `object.#{field_name}`. This is the default, and the method name can be overridden with `method: ...`:
5652

53+
```ruby
54+
field :title, String # calls object.title
55+
field :title, String, method: :get_title_somehow # calls object.get_title_somehow
56+
```
5757

58-
```ruby
59-
field :title, String, hash_key: :title # calls object[:title]
60-
field :title, String, hash_key: "title" # calls object["title"]
61-
```
58+
### Hash keys (`hash_key:`)
6259

63-
(Note: new execution doesn't "fall back" to hash key lookups, and it doesn't try strings when Symbols are given. The existing runtime engine does that...)
60+
Fields that call `object[hash_key]`, configured with `hash_key: ...`.
6461

65-
- __Batch resolvers__: fields that use a _class method_ to map parent objects to field results, configured with `resolve_batch:`:
62+
```ruby
63+
field :title, String, hash_key: :title # calls object[:title]
64+
field :title, String, hash_key: "title" # calls object["title"]
65+
```
6666

67-
```ruby
68-
field :title, String, resolve_batch: :titles do
69-
argument :language, Types::Language, required: false, default_value: "EN"
70-
end
67+
(Note: new execution doesn't "fall back" to hash key lookups, and it doesn't try strings when Symbols are given. The existing runtime engine does that...)
7168

72-
def self.titles(objects, context, language:)
73-
# This is equivalent to plain `field :title, ...`, but for example:
74-
objects.map { |obj| obj.title(language:) }
75-
end
76-
```
69+
### Per-object (`resolve_each:`)
7770

78-
This is especially useful when batching Dataloader calls:
71+
These fields use a _class method_ to produce a result for each parent object, configured with `resolve_each:`.
7972

80-
```ruby
81-
class Types::Comment < BaseObject
82-
field :post, Types::Post, resolve_batch: :posts
73+
```ruby
74+
field :title, String, resolve_each: :title do
75+
argument :language, Types::Language, required: false, default_value: "EN"
76+
end
8377

84-
# Use `.load_all(ids)` to fetch all in a single round-trip
85-
def self.posts(objects, context)
86-
# TODO: add a shorthand for this in GraphQL-Ruby
87-
context.dataloader
88-
.with(GraphQL::Dataloader::ActiveRecordSource)
89-
.load_all(objects.map(&:post_id))
90-
end
91-
end
92-
```
78+
def self.title(object, context, language:)
79+
# Assuming this makes no database lookups or other external service calls:
80+
object.localization.get(:title, language:)
81+
end
82+
```
9383

94-
- __Each resolvers__: fields that use a _class method_ to produce a result for each parent object, configured with `resolve_each:`. This is similar to `resolve_batch:`, except you never receive the whole list of `objects`:
84+
Under the hood, GraphQL-Ruby calls `objects.map { ... }`, calling this class method.
9585

96-
```ruby
97-
field :title, String, resolve_each: :title do
98-
argument :language, Types::Language, required: false, default_value: "EN"
99-
end
86+
‼️ __Don't use this__ if your logic calls external services or databases (including with Dataloader). If you do, your I/O will be sequential instead of batched. Use `resolve_batch:` or `resolve_static:` instead, see below.
10087

101-
def self.title(object, context, language:)
102-
object.title(language:)
103-
end
104-
```
88+
### Global (`resolve_static:`)
10589

106-
(Under the hood, GraphQL-Ruby calls `objects.map { ... }`, calling this class method.)
90+
Fields that use a _class method_ to produce a single result shared by all objects, configured with `resolve_static:`. The method does _not_ receive any `object`, only `context`:
10791

108-
- __Static resolvers__: fields that use a _class method_ to produce a single result shared by all objects, configured with `resolve_static:`. The method does _not_ receive any `object`, only `context`:
92+
```ruby
93+
field :posts_count, Integer, resolve_static: :count_all_posts do
94+
argument :include_unpublished, Boolean, required: false, default_value: false
95+
end
10996

110-
```ruby
111-
field :posts_count, Integer, resolve_static: :count_all_posts do
112-
argument :include_unpublished, Boolean, required: false, default_value: false
113-
end
97+
def self.count_all_posts(context, include_unpublished:)
98+
posts = Post.all
99+
if !include_unpublished
100+
posts = posts.published
101+
end
102+
posts.count
103+
end
104+
```
105+
106+
Under the hood, GraphQL-Ruby calls `Array.new(objects.size, static_result)`.
114107

115-
def self.count_all_posts(context, include_unpublished:)
116-
posts = Post.all
117-
if !include_unpublished
118-
posts = posts.published
119-
end
120-
posts.count
108+
### Batch resolvers (`resolve_batch:`)
109+
110+
This is a high-performance option for when you need to do I/O to generate results. By working with a batch of objects, you can greatly reduce the framework overhead in preparing a result.
111+
112+
These fields use a _class method_ to map parent objects to field results, configured with `resolve_batch:`:
113+
114+
```ruby
115+
field :title, String, resolve_batch: :titles do
116+
argument :language, Types::Language, required: false, default_value: "EN"
117+
end
118+
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+
```
124+
125+
This is especially useful when batching Dataloader calls:
126+
127+
```ruby
128+
class Types::Comment < BaseObject
129+
field :post, Types::Post, resolve_batch: :posts
130+
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))
121137
end
122-
```
138+
end
139+
```
140+
141+
### Legacy instance methods
142+
143+
`resolve_legacy_instance_method:`
123144

124-
(Under the hood, GraphQL-Ruby calls `Array.new(objects.size, static_result)`)
145+
There is _partial_ support for instance methods on Object type classes, for now. It will be deprecated and removed soon.
146+
147+
‼️ Don't use legacy instance methods with Dataloader. It will be sequential, not batched. ‼️
148+
149+
```ruby
150+
field :title, String, resolve_legacy_instance_method: true do
151+
argument :language, Types::Language, required: false, default_value: "EN"
152+
end
153+
154+
def title(language:)
155+
# Assuming this makes no database lookups or other external service calls:
156+
object.localization.get(:title, language:)
157+
end
158+
```
159+
160+
Under the hood, GraphQL-Ruby calls `objects.map { ... }`, calling this instance method.
125161

126162

127163
### `true` shorthand
128164

129-
There is also a `true` shorthand: when one of the `resolve_...:` configurations is passed as `true` (ie, `resolve_batch: true`, `resolve_each: true`, or `resolve_static: true`), then the Symbol field name is used as the class method. For example:
165+
There is also a `true` shorthand: when one of the `resolve_...:` configurations is passed as `true` (ie, `resolve_batch: true`, `resolve_each: true`, `resolve_static: true`, or `resolve_legacy_instance_method: true`), then the Symbol field name is used as the class method. For example:
130166

131167
```ruby
132168
field :posts_count, Integer, resolve_static: true
@@ -136,6 +172,7 @@ def self.posts_count(context)
136172
end
137173
```
138174

175+
139176
## Migration
140177

141178
Read about migrating in the {% internal_link "Migration Doc", "/execution/migration" %}.

lib/graphql/execution/next.rb

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
# frozen_string_literal: true
22
require "graphql/execution/next/prepare_object_step"
3-
require "graphql/execution/next/field_compatibility"
43
require "graphql/execution/next/field_resolve_step"
54
require "graphql/execution/next/load_argument_step"
65
require "graphql/execution/next/runner"

0 commit comments

Comments
 (0)