Skip to content

Commit d9d9d3f

Browse files
Skip preloading of polymorphic relations, better handling of serialization with partially preloaded relations, resolves #889
1 parent acac529 commit d9d9d3f

4 files changed

Lines changed: 81 additions & 22 deletions

File tree

lib/jsonapi/cached_resource_fragment.rb

Lines changed: 28 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,14 @@ def self.fetch_fragments(resource_klass, serializer, context, cache_ids)
66
context_b64 = JSONAPI.configuration.resource_cache_digest_function.call(context_json)
77
context_key = "ATTR-CTX-#{context_b64.gsub("/", "_")}"
88

9-
results = self.lookup(resource_klass, serializer_config_key, context_key, cache_ids)
9+
results = self.lookup(resource_klass, serializer_config_key, context, context_key, cache_ids)
1010

1111
miss_ids = results.select{|k,v| v.nil? }.keys
1212
unless miss_ids.empty?
1313
find_filters = {resource_klass._primary_key => miss_ids.uniq}
1414
find_options = {context: context}
1515
resource_klass.find(find_filters, find_options).each do |resource|
16-
(id, cr) = write(resource_klass, resource, serializer, serializer_config_key, context_key)
16+
(id, cr) = write(resource_klass, resource, serializer, serializer_config_key, context, context_key)
1717
results[id] = cr
1818
end
1919
end
@@ -29,28 +29,16 @@ def self.fetch_fragments(resource_klass, serializer, context, cache_ids)
2929
return results
3030
end
3131

32-
def self.from_cache_value(resource_klass, h)
33-
new(
34-
resource_klass,
35-
h.fetch(:id),
36-
h.fetch(:type),
37-
h.fetch(:fetchable),
38-
h.fetch(:rels, nil),
39-
h.fetch(:links, nil),
40-
h.fetch(:attrs, nil),
41-
h.fetch(:meta, nil)
42-
)
43-
end
44-
45-
attr_reader :resource_klass, :id, :type, :fetchable_fields, :relationships,
32+
attr_reader :resource_klass, :id, :type, :context, :fetchable_fields, :relationships,
4633
:links_json, :attributes_json, :meta_json,
4734
:preloaded_fragments
4835

49-
def initialize(resource_klass, id, type, fetchable_fields, relationships,
36+
def initialize(resource_klass, id, type, context, fetchable_fields, relationships,
5037
links_json, attributes_json, meta_json)
5138
@resource_klass = resource_klass
5239
@id = id
5340
@type = type
41+
@context = context
5442
@fetchable_fields = Set.new(fetchable_fields)
5543

5644
# Relationships left uncompiled because we'll often want to insert included ids on retrieval
@@ -76,9 +64,14 @@ def to_cache_value
7664
}
7765
end
7866

67+
def to_real_resource
68+
rs = Resource.resource_for(self.type).find_by_keys([self.id], {context: self.context})
69+
return rs.try(:first)
70+
end
71+
7972
private
8073

81-
def self.lookup(resource_klass, serializer_config_key, context_key, cache_ids)
74+
def self.lookup(resource_klass, serializer_config_key, context, context_key, cache_ids)
8275
type = resource_klass._type
8376

8477
keys = cache_ids.map do |(id, cache_key)|
@@ -89,20 +82,35 @@ def self.lookup(resource_klass, serializer_config_key, context_key, cache_ids)
8982
return keys.each_with_object({}) do |key, hash|
9083
(_, id, _, _) = key
9184
if hits.has_key?(key)
92-
hash[id] = self.from_cache_value(resource_klass, hits[key])
85+
hash[id] = self.from_cache_value(resource_klass, context, hits[key])
9386
else
9487
hash[id] = nil
9588
end
9689
end
9790
end
9891

99-
def self.write(resource_klass, resource, serializer, serializer_config_key, context_key)
92+
def self.from_cache_value(resource_klass, context, h)
93+
new(
94+
resource_klass,
95+
h.fetch(:id),
96+
h.fetch(:type),
97+
context,
98+
h.fetch(:fetchable),
99+
h.fetch(:rels, nil),
100+
h.fetch(:links, nil),
101+
h.fetch(:attrs, nil),
102+
h.fetch(:meta, nil)
103+
)
104+
end
105+
106+
def self.write(resource_klass, resource, serializer, serializer_config_key, context, context_key)
100107
(id, cache_key) = resource.cache_id
101108
json = serializer.object_hash(resource) # No inclusions passed to object_hash
102109
cr = self.new(
103110
resource_klass,
104111
json['id'],
105112
json['type'],
113+
context,
106114
resource.fetchable_fields,
107115
json['relationships'],
108116
json['links'],

lib/jsonapi/resource.rb

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1133,8 +1133,15 @@ def preload_included_fragments(resources, records, serializer, options)
11331133

11341134
# For each step on the path, figure out what the actual table name/alias in the join
11351135
# will be, and include the primary key of that table in our list of fields to select
1136+
non_polymorphic = true
11361137
path.each do |elem|
11371138
relationship = klass._relationships[elem]
1139+
if relationship.polymorphic
1140+
# Can't preload through a polymorphic belongs_to association, ResourceSerializer
1141+
# will just have to bypass the cache and load the real Resource.
1142+
non_polymorphic = false
1143+
break
1144+
end
11381145
assocs_path << relationship.relation_name(options).to_sym
11391146
# Converts [:a, :b, :c] to Rails-style { :a => { :b => :c }}
11401147
ar_hash = assocs_path.reverse.reduce{|memo, step| { step => memo } }
@@ -1148,6 +1155,7 @@ def preload_included_fragments(resources, records, serializer, options)
11481155
klass = relationship.resource_klass
11491156
pluck_attrs << table[klass._primary_key]
11501157
end
1158+
next unless non_polymorphic
11511159

11521160
# Pre-fill empty hashes for each resource up to the end of the path.
11531161
# This allows us to later distinguish between a preload that returned nothing

lib/jsonapi/resource_serializer.rb

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -298,8 +298,11 @@ def cached_relationships_hash(source, include_directives)
298298
h = source.relationships || {}
299299
return h unless include_directives.has_key?(:include_related)
300300

301-
relationships = source.resource_klass._relationships.select{|k,v| source.fetchable_fields.include?(k) }
301+
relationships = source.resource_klass._relationships.select do |k,v|
302+
source.fetchable_fields.include?(k)
303+
end
302304

305+
real_res = nil
303306
relationships.each do |rel_name, relationship|
304307
key = @key_formatter.format(rel_name)
305308
to_many = relationship.is_a? JSONAPI::Relationship::ToMany
@@ -310,7 +313,17 @@ def cached_relationships_hash(source, include_directives)
310313
h[key][:data] = to_many ? [] : nil
311314
end
312315

313-
source.preloaded_fragments[key].each do |id, f|
316+
fragments = source.preloaded_fragments[key]
317+
if fragments.nil?
318+
# The resources we want were not preloaded, we'll have to bypass the cache.
319+
# This happens when including through belongs_to polymorphic relationships
320+
if real_res.nil?
321+
real_res = source.to_real_resource
322+
end
323+
relation_resources = [real_res.public_send(rel_name)].flatten(1).compact
324+
fragments = relation_resources.map{|r| [r.id, r]}.to_h
325+
end
326+
fragments.each do |id, f|
314327
add_resource(f, ia)
315328

316329
if h.has_key?(key)

test/controllers/controller_test.rb

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1923,6 +1923,36 @@ def test_tags_show_multiple_with_nonexistent_ids_at_the_beginning
19231923
end
19241924
end
19251925

1926+
class PicturesControllerTest < ActionController::TestCase
1927+
def test_pictures_index
1928+
assert_cacheable_get :index
1929+
assert_response :success
1930+
assert_equal 3, json_response['data'].size
1931+
end
1932+
1933+
def test_pictures_index_with_polymorphic_include_one_level
1934+
assert_cacheable_get :index, params: {include: 'imageable'}
1935+
assert_response :success
1936+
assert_equal 3, json_response['data'].size
1937+
assert_equal 2, json_response['included'].size
1938+
end
1939+
end
1940+
1941+
class DocumentsControllerTest < ActionController::TestCase
1942+
def test_documents_index
1943+
assert_cacheable_get :index
1944+
assert_response :success
1945+
assert_equal 1, json_response['data'].size
1946+
end
1947+
1948+
def test_documents_index_with_polymorphic_include_one_level
1949+
assert_cacheable_get :index, params: {include: 'pictures'}
1950+
assert_response :success
1951+
assert_equal 1, json_response['data'].size
1952+
assert_equal 1, json_response['included'].size
1953+
end
1954+
end
1955+
19261956
class ExpenseEntriesControllerTest < ActionController::TestCase
19271957
def setup
19281958
JSONAPI.configuration.json_key_format = :camelized_key

0 commit comments

Comments
 (0)