Skip to content

Commit 2145540

Browse files
richmoljNullVoxPopuli
authored andcommitted
Add include_data :if_sideloaded (#1931)
For JSONAPI, `include_data` currently means, "should we populate the 'data'" key for this relationship. Current options are true/false. This adds the `:if_sideloaded` option. This means "only populate the 'data' key when we are sideloading this relationship." This is because 'data' is often only relevant to sideloading, and causes a database hit. Addresses #1555
1 parent 6ed499f commit 2145540

File tree

6 files changed

+204
-28
lines changed

6 files changed

+204
-28
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ Features:
1717
- Added `jsonapi_namespace_separator` config option.
1818
- [#1889](https://github.com/rails-api/active_model_serializers/pull/1889) Support key transformation for Attributes adapter (@iancanderson, @danbee)
1919
- [#1917](https://github.com/rails-api/active_model_serializers/pull/1917) Add `jsonapi_pagination_links_enabled` configuration option (@richmolj)
20+
- [#1797](https://github.com/rails-api/active_model_serializers/pull/1797) Only include 'relationships' when sideloading (@richmolj)
2021

2122
Fixes:
2223

lib/active_model/serializer/concerns/associations.rb

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -83,15 +83,17 @@ def associate(reflection)
8383
# +default_include_directive+ config value when not provided)
8484
# @return [Enumerator<Association>]
8585
#
86-
def associations(include_directive = ActiveModelSerializers.default_include_directive)
86+
def associations(include_directive = ActiveModelSerializers.default_include_directive, include_slice = nil)
87+
include_slice ||= include_directive
8788
return unless object
8889

8990
Enumerator.new do |y|
9091
self.class._reflections.values.each do |reflection|
9192
next if reflection.excluded?(self)
9293
key = reflection.options.fetch(:key, reflection.name)
9394
next unless include_directive.key?(key)
94-
y.yield reflection.build_association(self, instance_options)
95+
96+
y.yield reflection.build_association(self, instance_options, include_slice)
9597
end
9698
end
9799
end

lib/active_model/serializer/concerns/configuration.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ def config.array_serializer
3030
# Make JSON API top-level jsonapi member opt-in
3131
# ref: http://jsonapi.org/format/#document-top-level
3232
config.jsonapi_include_toplevel_object = false
33+
config.include_data_default = true
3334

3435
config.schema_path = 'test/support/schemas'
3536
end

lib/active_model/serializer/reflection.rb

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ class Reflection < Field
3737
def initialize(*)
3838
super
3939
@_links = {}
40-
@_include_data = true
40+
@_include_data = Serializer.config.include_data_default
4141
@_meta = nil
4242
end
4343

@@ -69,17 +69,15 @@ def include_data(value = true)
6969
# Blog.find(object.blog_id)
7070
# end
7171
# end
72-
def value(serializer)
72+
def value(serializer, include_slice)
7373
@object = serializer.object
7474
@scope = serializer.scope
7575

76-
if block
77-
block_value = instance_exec(serializer, &block)
78-
if block_value != :nil
79-
block_value
80-
elsif @_include_data
81-
serializer.read_attribute_for_serialization(name)
82-
end
76+
block_value = instance_exec(serializer, &block) if block
77+
return unless include_data?(include_slice)
78+
79+
if block && block_value != :nil
80+
block_value
8381
else
8482
serializer.read_attribute_for_serialization(name)
8583
end
@@ -106,11 +104,11 @@ def value(serializer)
106104
#
107105
# @api private
108106
#
109-
def build_association(parent_serializer, parent_serializer_options)
110-
association_value = value(parent_serializer)
107+
def build_association(parent_serializer, parent_serializer_options, include_slice = {})
108+
association_value = value(parent_serializer, include_slice)
111109
reflection_options = options.dup
112110
serializer_class = parent_serializer.class.serializer_for(association_value, reflection_options)
113-
reflection_options[:include_data] = @_include_data
111+
reflection_options[:include_data] = include_data?(include_slice)
114112
reflection_options[:links] = @_links
115113
reflection_options[:meta] = @_meta
116114

@@ -137,6 +135,14 @@ def build_association(parent_serializer, parent_serializer_options)
137135

138136
private
139137

138+
def include_data?(include_slice)
139+
if @_include_data == :if_sideloaded
140+
include_slice.key?(name)
141+
else
142+
@_include_data
143+
end
144+
end
145+
140146
def serializer_options(parent_serializer, parent_serializer_options, reflection_options)
141147
serializer = reflection_options.fetch(:serializer, nil)
142148

lib/active_model_serializers/adapter/json_api.rb

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -235,17 +235,17 @@ def resource_objects_for(serializers)
235235
@primary = []
236236
@included = []
237237
@resource_identifiers = Set.new
238-
serializers.each { |serializer| process_resource(serializer, true) }
238+
serializers.each { |serializer| process_resource(serializer, true, @include_directive) }
239239
serializers.each { |serializer| process_relationships(serializer, @include_directive) }
240240

241241
[@primary, @included]
242242
end
243243

244-
def process_resource(serializer, primary)
244+
def process_resource(serializer, primary, include_slice = {})
245245
resource_identifier = ResourceIdentifier.new(serializer, instance_options).as_json
246246
return false unless @resource_identifiers.add?(resource_identifier)
247247

248-
resource_object = resource_object_for(serializer)
248+
resource_object = resource_object_for(serializer, include_slice)
249249
if primary
250250
@primary << resource_object
251251
else
@@ -255,21 +255,21 @@ def process_resource(serializer, primary)
255255
true
256256
end
257257

258-
def process_relationships(serializer, include_directive)
259-
serializer.associations(include_directive).each do |association|
260-
process_relationship(association.serializer, include_directive[association.key])
258+
def process_relationships(serializer, include_slice)
259+
serializer.associations(include_slice).each do |association|
260+
process_relationship(association.serializer, include_slice[association.key])
261261
end
262262
end
263263

264-
def process_relationship(serializer, include_directive)
264+
def process_relationship(serializer, include_slice)
265265
if serializer.respond_to?(:each)
266-
serializer.each { |s| process_relationship(s, include_directive) }
266+
serializer.each { |s| process_relationship(s, include_slice) }
267267
return
268268
end
269269
return unless serializer && serializer.object
270-
return unless process_resource(serializer, false)
270+
return unless process_resource(serializer, false, include_slice)
271271

272-
process_relationships(serializer, include_directive)
272+
process_relationships(serializer, include_slice)
273273
end
274274

275275
# {http://jsonapi.org/format/#document-resource-object-attributes Document Resource Object Attributes}
@@ -293,7 +293,7 @@ def attributes_for(serializer, fields)
293293
end
294294

295295
# {http://jsonapi.org/format/#document-resource-objects Document Resource Objects}
296-
def resource_object_for(serializer)
296+
def resource_object_for(serializer, include_slice = {})
297297
resource_object = serializer.fetch(self) do
298298
resource_object = ResourceIdentifier.new(serializer, instance_options).as_json
299299

@@ -304,7 +304,7 @@ def resource_object_for(serializer)
304304
end
305305

306306
requested_associations = fieldset.fields_for(resource_object[:type]) || '*'
307-
relationships = relationships_for(serializer, requested_associations)
307+
relationships = relationships_for(serializer, requested_associations, include_slice)
308308
resource_object[:relationships] = relationships if relationships.any?
309309

310310
links = links_for(serializer)
@@ -432,12 +432,12 @@ def resource_object_for(serializer)
432432
# id: 'required-id',
433433
# meta: meta
434434
# }.reject! {|_,v| v.nil? }
435-
def relationships_for(serializer, requested_associations)
435+
def relationships_for(serializer, requested_associations, include_slice)
436436
include_directive = JSONAPI::IncludeDirective.new(
437437
requested_associations,
438438
allow_wildcard: true
439439
)
440-
serializer.associations(include_directive).each_with_object({}) do |association, hash|
440+
serializer.associations(include_directive, include_slice).each_with_object({}) do |association, hash|
441441
hash[association.key] = Relationship.new(serializer, instance_options, association).as_json
442442
end
443443
end
Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
require 'test_helper'
2+
3+
module ActiveModel
4+
class Serializer
5+
module Adapter
6+
class JsonApi
7+
class IncludeParamTest < ActiveSupport::TestCase
8+
IncludeParamAuthor = Class.new(::Model)
9+
10+
class CustomCommentLoader
11+
def all
12+
[{ foo: 'bar' }]
13+
end
14+
end
15+
16+
class TagSerializer < ActiveModel::Serializer
17+
attributes :id, :name
18+
end
19+
20+
class IncludeParamAuthorSerializer < ActiveModel::Serializer
21+
class_attribute :comment_loader
22+
23+
has_many :tags, serializer: TagSerializer do
24+
link :self, '//example.com/link_author/relationships/tags'
25+
include_data :if_sideloaded
26+
end
27+
28+
has_many :unlinked_tags, serializer: TagSerializer do
29+
include_data :if_sideloaded
30+
end
31+
32+
has_many :posts, serializer: PostWithTagsSerializer do
33+
include_data :if_sideloaded
34+
end
35+
has_many :locations do
36+
include_data :if_sideloaded
37+
end
38+
has_many :comments do
39+
include_data :if_sideloaded
40+
IncludeParamAuthorSerializer.comment_loader.all
41+
end
42+
end
43+
44+
def setup
45+
IncludeParamAuthorSerializer.comment_loader = Class.new(CustomCommentLoader).new
46+
@tag = Tag.new(id: 1337, name: 'mytag')
47+
@author = IncludeParamAuthor.new(
48+
id: 1337,
49+
tags: [@tag]
50+
)
51+
end
52+
53+
def test_relationship_not_loaded_when_not_included
54+
expected = {
55+
links: {
56+
self: '//example.com/link_author/relationships/tags'
57+
}
58+
}
59+
60+
@author.define_singleton_method(:read_attribute_for_serialization) do |attr|
61+
fail 'should not be called' if attr == :tags
62+
super(attr)
63+
end
64+
65+
assert_relationship(:tags, expected)
66+
end
67+
68+
def test_relationship_included
69+
expected = {
70+
data: [
71+
{
72+
id: '1337',
73+
type: 'tags'
74+
}
75+
],
76+
links: {
77+
self: '//example.com/link_author/relationships/tags'
78+
}
79+
}
80+
81+
assert_relationship(:tags, expected, include: :tags)
82+
end
83+
84+
def test_sideloads_included
85+
expected = [
86+
{
87+
id: '1337',
88+
type: 'tags',
89+
attributes: { name: 'mytag' }
90+
}
91+
]
92+
hash = result(include: :tags)
93+
assert_equal(expected, hash[:included])
94+
end
95+
96+
def test_nested_relationship
97+
expected = {
98+
data: [
99+
{
100+
id: '1337',
101+
type: 'tags'
102+
}
103+
],
104+
links: {
105+
self: '//example.com/link_author/relationships/tags'
106+
}
107+
}
108+
109+
expected_no_data = {
110+
links: {
111+
self: '//example.com/link_author/relationships/tags'
112+
}
113+
}
114+
115+
assert_relationship(:tags, expected, include: [:tags, { posts: :tags }])
116+
117+
@author.define_singleton_method(:read_attribute_for_serialization) do |attr|
118+
fail 'should not be called' if attr == :tags
119+
super(attr)
120+
end
121+
122+
assert_relationship(:tags, expected_no_data, include: { posts: :tags })
123+
end
124+
125+
def test_include_params_with_no_block
126+
@author.define_singleton_method(:read_attribute_for_serialization) do |attr|
127+
fail 'should not be called' if attr == :locations
128+
super(attr)
129+
end
130+
131+
expected = { meta: {} }
132+
133+
assert_relationship(:locations, expected)
134+
end
135+
136+
def test_block_relationship
137+
expected = {
138+
data: [
139+
{ 'foo' => 'bar' }
140+
]
141+
}
142+
143+
assert_relationship(:comments, expected, include: [:comments])
144+
end
145+
146+
def test_node_not_included_when_no_link
147+
expected = nil
148+
assert_relationship(:unlinked_tags, expected)
149+
end
150+
151+
private
152+
153+
def result(opts)
154+
opts = { adapter: :json_api }.merge(opts)
155+
serializable(@author, opts).serializable_hash
156+
end
157+
158+
def assert_relationship(relationship_name, expected, opts = {})
159+
hash = result(opts)
160+
assert_equal(expected, hash[:data][:relationships][relationship_name])
161+
end
162+
end
163+
end
164+
end
165+
end
166+
end

0 commit comments

Comments
 (0)