Skip to content

Commit 668ebc1

Browse files
Dataloader support
1 parent 91704e9 commit 668ebc1

File tree

6 files changed

+106
-0
lines changed

6 files changed

+106
-0
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/object_helpers.rb

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,11 @@ def cache_fragment(object_to_cache = NO_OBJECT, **options, &block)
4848

4949
fragment = Fragment.new(context_to_use, **options)
5050

51+
if options.delete(:dataloader)
52+
object_to_cache = block.call
53+
block = nil
54+
end
55+
5156
GraphQL::FragmentCache::Schema::LazyCacheResolver.new(fragment, context_to_use, object_to_cache, &block)
5257
end
5358
end

spec/graphql/fragment_cache/object_helpers_spec.rb

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -777,6 +777,59 @@ 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+
before do
808+
allow(User).to receive(:find_by_post_ids).and_call_original
809+
810+
# warmup cache
811+
execute_query
812+
expect(User).to have_received(:find_by_post_ids).with([post1.id, post2.id])
813+
814+
# make objects dirty
815+
user1.name = "User #1 new"
816+
user2.name = "User #2 new"
817+
end
818+
819+
it "returns cached results" do
820+
expect(execute_query.dig("data", "posts")).to eq([
821+
{
822+
"id" => "1",
823+
"dataloaderCachedAuthor" => {"name" => "User #1"}
824+
},
825+
{
826+
"id" => "2",
827+
"dataloaderCachedAuthor" => {"name" => "User #2"}
828+
}
829+
])
830+
end
831+
end
832+
780833
describe "conditional caching" do
781834
let(:schema) do
782835
field_resolver = resolver

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)