Skip to content
This repository was archived by the owner on Oct 26, 2022. It is now read-only.

Commit 828b8c7

Browse files
committed
Fix ActiveRecord connection caching
1 parent 837bb75 commit 828b8c7

File tree

8 files changed

+140
-89
lines changed

8 files changed

+140
-89
lines changed

lib/graphql/cache/fetcher.rb

+3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
# frozen_string_literal: true
22

3+
require 'graphql/cache/resolvers/base_resolver'
4+
require 'graphql/cache/resolvers/scalar_resolver'
5+
require 'graphql/cache/resolvers/connection_resolver'
36
require 'graphql/cache/resolver'
47

58
module GraphQL

lib/graphql/cache/marshal.rb

+5-16
Original file line numberDiff line numberDiff line change
@@ -24,23 +24,12 @@ def initialize(key)
2424
self.key = key.to_s
2525
end
2626

27-
# Read a value from cache if it exists and re-hydrate it or
28-
# execute the block and write it's result to cache
29-
#
30-
# @param config [Hash] The object passed to `cache:` on the field definition
27+
# Read a value from cache
3128
# @return [Object]
32-
def read(config, force: false, &block)
33-
# write new data from resolver if forced
34-
return write(config, &block) if force
35-
36-
cached = cache.read(key)
37-
38-
if cached.nil?
39-
logger.debug "Cache miss: (#{key})"
40-
write config, &block
41-
else
42-
logger.debug "Cache hit: (#{key})"
43-
cached
29+
def read
30+
cache.read(key).tap do |cached|
31+
logger.debug "Cache miss: (#{key})" if cached.nil?
32+
logger.debug "Cache hit: (#{key})" if cached
4433
end
4534
end
4635

lib/graphql/cache/resolver.rb

+10-40
Original file line numberDiff line numberDiff line change
@@ -4,29 +4,25 @@ module GraphQL
44
module Cache
55
# Represents the caching resolver that wraps the existing resolver proc
66
class Resolver
7-
attr_accessor :type
8-
9-
attr_accessor :field
10-
11-
attr_accessor :orig_resolve_proc
7+
attr_accessor :type, :field, :orig_resolve_proc
128

139
def initialize(type, field)
1410
@type = type
1511
@field = field
1612
end
1713

1814
def call(obj, args, ctx)
19-
@orig_resolve_proc = field.resolve_proc
20-
15+
resolve_proc = proc { field.resolve_proc.call(obj, args, ctx) }
2116
key = cache_key(obj, args, ctx)
22-
23-
value = Marshal[key].read(
24-
field.metadata[:cache], force: ctx[:force_cache]
25-
) do
26-
@orig_resolve_proc.call(obj, args, ctx)
17+
metadata = field.metadata[:cache]
18+
19+
if field.connection?
20+
Resolvers::ConnectionResolver.new(resolve_proc, key, metadata).call(
21+
args: args, field: field, parent: obj, context: ctx, force_cache: ctx[:force_cache]
22+
)
23+
else
24+
Resolvers::ScalarResolver.new(resolve_proc, key, metadata).call(force_cache: ctx[:force_cache])
2725
end
28-
29-
wrap_connections(value, args, parent: obj, context: ctx)
3026
end
3127

3228
protected
@@ -35,32 +31,6 @@ def call(obj, args, ctx)
3531
def cache_key(obj, args, ctx)
3632
Key.new(obj, args, type, field, ctx).to_s
3733
end
38-
39-
# @private
40-
def wrap_connections(value, args, **kwargs)
41-
# return raw value if field isn't a connection (no need to wrap)
42-
return value unless field.connection?
43-
44-
# return cached value if it is already a connection object
45-
# this occurs when the value is being resolved by GraphQL
46-
# and not being read from cache
47-
return value if value.class.ancestors.include?(
48-
GraphQL::Relay::BaseConnection
49-
)
50-
51-
create_connection(value, args, **kwargs)
52-
end
53-
54-
# @private
55-
def create_connection(value, args, **kwargs)
56-
GraphQL::Relay::BaseConnection.connection_for_nodes(value).new(
57-
value,
58-
args,
59-
field: field,
60-
parent: kwargs[:parent],
61-
context: kwargs[:context]
62-
)
63-
end
6434
end
6535
end
6636
end
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# frozen_string_literal: true
2+
3+
module GraphQL
4+
module Cache
5+
module Resolvers
6+
class BaseResolver
7+
def initialize(resolve_proc, key, metadata)
8+
@resolve_proc = resolve_proc
9+
@key = key
10+
@metadata = metadata
11+
end
12+
13+
def call(*args)
14+
raise NotImplementedError
15+
end
16+
17+
private
18+
19+
attr_reader :resolve_proc, :key, :metadata
20+
21+
def read
22+
Marshal[key].read
23+
end
24+
25+
def write(&block)
26+
Marshal[key].write(metadata, &block)
27+
end
28+
end
29+
end
30+
end
31+
end
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
# frozen_string_literal: true
2+
3+
module GraphQL
4+
module Cache
5+
module Resolvers
6+
# Pass cache write method into GraphQL::Relay::BaseConnection
7+
# and wrap them original Connection methods
8+
class ConnectionResolver < BaseResolver
9+
class ConnectionCache < Module
10+
module WrappedMethods
11+
def paged_nodes
12+
cache_write = instance_variable_get(:@__cache_write)
13+
14+
cache_write.call { super }
15+
end
16+
end
17+
18+
def initialize(write)
19+
@write = write
20+
end
21+
22+
def extended(base)
23+
base.extend(WrappedMethods)
24+
base.instance_variable_set(:@__cache_write, @write)
25+
end
26+
end
27+
28+
def call(args:, field:, parent:, context:, force_cache:)
29+
if force_cache || (cached = read).nil?
30+
define_connection_cache(resolve_proc.call)
31+
else
32+
wrap_connection(cached, args, field, parent: parent, context: context)
33+
end
34+
end
35+
36+
private
37+
38+
def wrap_connection(value, args, field, **kwargs)
39+
GraphQL::Relay::BaseConnection.connection_for_nodes(value).new(
40+
value,
41+
args,
42+
field: field,
43+
parent: kwargs[:parent],
44+
context: kwargs[:context]
45+
)
46+
end
47+
48+
def define_connection_cache(connection)
49+
connection.extend(ConnectionCache.new(method(:write)))
50+
end
51+
end
52+
end
53+
end
54+
end
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# frozen_string_literal: true
2+
3+
module GraphQL
4+
module Cache
5+
module Resolvers
6+
class ScalarResolver < BaseResolver
7+
def call(force_cache:)
8+
return write if force_cache
9+
10+
cached = read
11+
12+
cached.nil? ? write { resolve_proc.call } : cached
13+
end
14+
end
15+
end
16+
end
17+
end

spec/features/connections_spec.rb

+16-4
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,19 @@ def messages
4040

4141
shared_examples "be a correct cold and warm" do
4242
let(:reference) do
43-
{"data" => {"customer" => {"orders" => {"edges" => [{"node" => {"id" =>1 }}, {"node" => {"id" => 2}}, {"node" => {"id" => 3}}]}}}}
43+
{
44+
"data" => {
45+
"customer" => {
46+
"orders" => {
47+
"edges" => [
48+
{"node" => {"id" => 1}},
49+
{"node" => {"id" => 2}},
50+
{"node" => {"id" => 3}}
51+
]
52+
}
53+
}
54+
}
55+
}
4456
end
4557

4658
it 'produces the same result on miss or hit' do
@@ -91,9 +103,9 @@ def execute(query, context = {})
91103

92104
expect(sql_logger.messages).to eq(
93105
<<~SQL
94-
SELECT "customers".* FROM "customers" ORDER BY "customers"."id" DESC LIMIT ? [["LIMIT", 1]]
95-
SELECT "customers".* FROM "customers" WHERE "customers"."id" = ? LIMIT ? [["id", 1], ["LIMIT", 1]]
96-
SELECT "orders".* FROM "orders" WHERE "orders"."customer_id" = ? [["customer_id", 1]]
106+
SELECT \"customers\".* FROM \"customers\" ORDER BY \"customers\".\"id\" DESC LIMIT ?\e[0m [[\"LIMIT\", 1]]
107+
SELECT \"customers\".* FROM \"customers\" WHERE \"customers\".\"id\" = ? LIMIT ?\e[0m [[\"id\", 1], [\"LIMIT\", 1]]
108+
SELECT \"orders\".* FROM \"orders\" WHERE \"orders\".\"customer_id\" = ?\e[0m [[\"customer_id\", 1]]
97109
SQL
98110
)
99111
end

spec/graphql/cache/marshal_spec.rb

+4-29
Original file line numberDiff line numberDiff line change
@@ -32,47 +32,22 @@ module Cache
3232

3333
describe '#read' do
3434
let(:config) { true }
35-
let(:block) { double('block', call: 'foo') }
36-
37-
context 'when force is set' do
38-
it 'should execute the block' do
39-
expect(block).to receive(:call)
40-
subject.read(config, force: true) { block.call }
41-
end
42-
43-
it 'should write to cache' do
44-
expect(cache).to receive(:write).with(key, doc, expires_in: GraphQL::Cache.expiry)
45-
subject.write(config) { doc }
46-
end
47-
end
4835

4936
context 'when cache object exists' do
5037
before do
5138
cache.write(key, doc)
5239
end
5340

5441
it 'should return cached value' do
55-
expect(subject.read(config) { block.call }).to eq doc
56-
end
57-
58-
it 'should not execute the block' do
59-
expect(block).to_not receive(:call)
60-
subject.read(config) { block.call }
42+
expect(subject.read).to eq doc
6143
end
6244
end
6345

6446
context 'when cache object does not exist' do
65-
before do
66-
cache.clear
67-
end
68-
69-
it 'should return the evaluated value' do
70-
expect(subject.read(config) { block.call }).to eq block.call
71-
end
47+
before { cache.clear }
7248

73-
it 'should execute the block' do
74-
expect(block).to receive(:call)
75-
subject.read(config) { block.call }
49+
it 'should return nil' do
50+
expect(subject.read).to be_nil
7651
end
7752
end
7853
end

0 commit comments

Comments
 (0)