Skip to content

Commit 743b542

Browse files
Dataloader support (#130)
1 parent 91704e9 commit 743b542

File tree

8 files changed

+128
-16
lines changed

8 files changed

+128
-16
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
## master
44

5+
- [PR#130](https://github.com/DmitryTsepelev/graphql-ruby-fragment_cache/pull/130) Dataloader support ([@DmitryTsepelev][])
56
- [PR#125](https://github.com/DmitryTsepelev/graphql-ruby-fragment_cache/pull/125) Introduce cache lookup instrumentation hook ([@danielhartnell][])
67

78
## 1.20.5 (2024-11-02)

README.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -381,6 +381,34 @@ class QueryType < BaseObject
381381
end
382382
```
383383

384+
## Dataloader
385+
386+
If you are using [Dataloader](https://graphql-ruby.org/dataloader/overview.html), you will need to let the gem know using `dataloader: true`:
387+
388+
```ruby
389+
class PostType < BaseObject
390+
field :author, User, null: false
391+
392+
def author
393+
cache_fragment(dataloader: true) do
394+
dataloader.with(AuthorDataloaderSource).load(object.id)
395+
end
396+
end
397+
end
398+
399+
# or
400+
401+
class PostType < BaseObject
402+
field :author, User, null: false, cache_fragment: {dataloader: true}
403+
404+
def author
405+
dataloader.with(AuthorDataloaderSource).load(object.id)
406+
end
407+
end
408+
```
409+
410+
The problem is that I didn't find a way to detect that dataloader (and, therefore, Fiber) is used, and the block is forced to resolve, causing the N+1 inside the Dataloader Source class.
411+
384412
## How to use `#cache_fragment` in extensions (and other places where context is not available)
385413

386414
If you want to call `#cache_fragment` from places other that fields or resolvers, you'll need to pass `context` explicitly and turn on `raw_value` support. For instance, let's take a look at this extension:

lib/graphql/fragment_cache/fragment.rb

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,10 @@ def read_multi(fragments)
1616
return fragments.map { |f| [f, f.read] }.to_h
1717
end
1818

19-
fragments_to_cache_keys = fragments
20-
.map { |f| [f, f.cache_key] }.to_h
19+
fragments_to_cache_keys = fragments.map { |f| [f, f.cache_key] }.to_h
2120

2221
# Filter out all the cache_keys for fragments with renew_cache: true in their context
23-
cache_keys = fragments_to_cache_keys
24-
.reject { |k, _v| k.context[:renew_cache] == true }.values
22+
cache_keys = fragments_to_cache_keys.reject { |k, _v| k.context[:renew_cache] == true }.values
2523

2624
# If there are cache_keys look up values with read_multi otherwise return an empty hash
2725
cache_keys_to_values = if cache_keys.empty?
@@ -46,8 +44,7 @@ def read_multi(fragments)
4644
end
4745

4846
# Fragmenst without values or with renew_cache: true in their context will have nil values like the read method
49-
fragments_to_cache_keys
50-
.map { |fragment, cache_key| [fragment, cache_keys_to_values[cache_key]] }.to_h
47+
fragments_to_cache_keys.map { |fragment, cache_key| [fragment, cache_keys_to_values[cache_key]] }.to_h
5148
end
5249
end
5350

lib/graphql/fragment_cache/schema/lazy_cache_resolver.rb

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ def initialize(fragment, query_ctx, object_to_cache, &block)
1616
@block = block
1717

1818
@lazy_state[:pending_fragments] << @fragment
19+
20+
ensure_dataloader_resulution! if @fragment.options[:dataloader]
1921
end
2022

2123
def resolve
@@ -35,6 +37,15 @@ def resolve
3537
@query_ctx.fragments << @fragment
3638
end
3739
end
40+
41+
private
42+
43+
def ensure_dataloader_resulution!
44+
return if FragmentCache.cache_store.exist?(@fragment.cache_key)
45+
46+
@object_to_cache = @block.call
47+
@block = nil
48+
end
3849
end
3950
end
4051
end

spec/graphql/fragment_cache/object_helpers_spec.rb

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -777,6 +777,62 @@ def post(id:, expires_in: nil)
777777
end
778778
end
779779

780+
describe "caching fields with dataloader" do
781+
let(:query) do
782+
<<~GQL
783+
query GetPosts {
784+
posts {
785+
id
786+
dataloaderCachedAuthor {
787+
name
788+
}
789+
}
790+
}
791+
GQL
792+
end
793+
794+
let(:schema) do
795+
build_schema do
796+
use GraphQL::Dataloader
797+
query(Types::Query)
798+
end
799+
end
800+
801+
let(:user1) { User.new(id: 1, name: "User #1") }
802+
let(:user2) { User.new(id: 2, name: "User #2") }
803+
804+
let!(:post1) { Post.create(id: 1, title: "object test 1", author: user1) }
805+
let!(:post2) { Post.create(id: 2, title: "object test 2", author: user2) }
806+
807+
let(:memory_store) { GraphQL::FragmentCache::MemoryStore.new }
808+
809+
before do
810+
allow(User).to receive(:find_by_post_ids).and_call_original
811+
812+
# warmup cache
813+
execute_query
814+
815+
# make objects dirty
816+
user1.name = "User #1 new"
817+
user2.name = "User #2 new"
818+
end
819+
820+
it "returns cached results" do
821+
expect(execute_query.dig("data", "posts")).to eq([
822+
{
823+
"id" => "1",
824+
"dataloaderCachedAuthor" => {"name" => "User #1"}
825+
},
826+
{
827+
"id" => "2",
828+
"dataloaderCachedAuthor" => {"name" => "User #2"}
829+
}
830+
])
831+
832+
expect(User).to have_received(:find_by_post_ids).with([post1.id, post2.id]).once
833+
end
834+
end
835+
780836
describe "conditional caching" do
781837
let(:schema) do
782838
field_resolver = resolver

spec/graphql/fragment_cache/schema/lazy_cache_resolver_spec.rb

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,21 +6,27 @@
66
describe "#initialize" do
77
context "lazy cache resolver state management" do
88
let(:state_key) { :lazy_cache_resolver_statez }
9+
let(:gql_context) { instance_double "Context" }
10+
let(:fragment) { GraphQL::FragmentCache::Fragment.new(gql_context) }
11+
12+
before do
13+
allow(gql_context).to receive(:namespace).and_return({})
14+
end
915

1016
it "adds lazy state property to the query context" do
1117
context = {}
1218

1319
expect(context).not_to have_key(state_key)
1420

15-
GraphQL::FragmentCache::Schema::LazyCacheResolver.new(nil, context, {})
21+
GraphQL::FragmentCache::Schema::LazyCacheResolver.new(fragment, context, {})
1622

1723
expect(context).to have_key(state_key)
1824
end
1925

2026
it "has :pending_fragments Set in state" do
2127
context = {}
2228

23-
GraphQL::FragmentCache::Schema::LazyCacheResolver.new({}, context, {})
29+
GraphQL::FragmentCache::Schema::LazyCacheResolver.new(fragment, context, {})
2430

2531
expect(context[state_key]).to have_key(:pending_fragments)
2632
expect(context[state_key][:pending_fragments]).to be_instance_of(Set)
@@ -29,7 +35,7 @@
2935
it "has :resolved_fragments Hash in state" do
3036
context = {}
3137

32-
GraphQL::FragmentCache::Schema::LazyCacheResolver.new({}, context, {})
38+
GraphQL::FragmentCache::Schema::LazyCacheResolver.new(fragment, context, {})
3339

3440
expect(context[state_key]).to have_key(:resolved_fragments)
3541
expect(context[state_key][:resolved_fragments]).to be_instance_of(Hash)
@@ -39,7 +45,7 @@
3945
context = {}
4046
fragments = []
4147

42-
3.times { fragments.push(Object.new) }
48+
3.times { fragments.push(GraphQL::FragmentCache::Fragment.new(gql_context)) }
4349

4450
fragments.each do |f|
4551
GraphQL::FragmentCache::Schema::LazyCacheResolver.new(f, context, {})
@@ -51,10 +57,4 @@
5157
end
5258
end
5359
end
54-
55-
it "has :resolve method" do
56-
lazy_cache_resolver = GraphQL::FragmentCache::Schema::LazyCacheResolver.new({}, {}, {})
57-
58-
expect(lazy_cache_resolver).to respond_to(:resolve)
59-
end
6060
end

spec/support/models/user.rb

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,12 @@ class User
44
attr_reader :id
55
attr_accessor :name
66

7+
class << self
8+
def find_by_post_ids(post_ids)
9+
post_ids.map { |id| Post.find(id).author }
10+
end
11+
end
12+
713
def initialize(id:, name:)
814
@id = id
915
@name = name

spec/support/test_schema.rb

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,12 @@ def perform(posts)
77
end
88
end
99

10+
class AuthorDataloaderSource < GraphQL::Dataloader::Source
11+
def fetch(post_ids)
12+
User.find_by_post_ids(post_ids)
13+
end
14+
end
15+
1016
module Types
1117
class Base < GraphQL::Schema::Object
1218
include GraphQL::FragmentCache::Object
@@ -41,6 +47,7 @@ class Post < Base
4147
field :cached_author, User, null: false
4248
field :batched_cached_author, User, null: false
4349
field :cached_author_inside_batch, User, null: false
50+
field :dataloader_cached_author, User, null: false
4451

4552
field :meta, String, null: true
4653

@@ -60,6 +67,12 @@ def cached_author_inside_batch
6067
cache_fragment(author, context: context)
6168
end
6269
end
70+
71+
def dataloader_cached_author
72+
cache_fragment(dataloader: true) do
73+
dataloader.with(AuthorDataloaderSource).load(object.id)
74+
end
75+
end
6376
end
6477

6578
class PostInput < GraphQL::Schema::InputObject

0 commit comments

Comments
 (0)