Skip to content

Commit 75eadc1

Browse files
committed
Allow you to disable eager loading on relationships mentioned in includes
1 parent cdcd04c commit 75eadc1

8 files changed

Lines changed: 81 additions & 32 deletions

File tree

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -541,6 +541,7 @@ The relationship methods (`relationship`, `has_one`, and `has_many`) support the
541541
* `polymorphic` - set to true to identify relationships that are polymorphic.
542542
* `relation_name` - the name of the relation to use on the model. A lambda may be provided which allows conditional selection of the relation based on the context.
543543
* `always_include_linkage_data` - if set to true, the relationship includes linkage data. Defaults to false if not set.
544+
* `eager_load_on_include` - if set to false, will not include this relationship in join SQL when requested via an include. You usually want to leave this on, but it will break 'relationships' which are not active record, for example if you want to expose a tree using the `ancestry` gem or similar, or the SQL query becomes too large to handle. Defaults to true if not set.
544545

545546
`to_one` relationships support the additional option:
546547
* `foreign_key_on` - defaults to `:self`. To indicate that the foreign key is on the related resource specify `:related`.

lib/jsonapi/include_directives.rb

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,9 @@ class IncludeDirectives
1919
# }
2020
# }
2121

22-
def initialize(includes_array)
22+
def initialize(resource_klass, includes_array, force_eager_load: false)
23+
@resource_klass = resource_klass
24+
@force_eager_load = force_eager_load
2325
@include_directives_hash = { include_related: {} }
2426
includes_array.each do |include|
2527
parse_include(include)
@@ -38,16 +40,27 @@ def model_includes
3840

3941
def get_related(current_path)
4042
current = @include_directives_hash
43+
current_resource_klass = @resource_klass
4144
current_path.split('.').each do |fragment|
4245
fragment = fragment.to_sym
43-
current[:include_related][fragment] ||= { include: false, include_related: {} }
46+
47+
if current_resource_klass
48+
current_relationship = current_resource_klass._relationships[fragment]
49+
current_resource_klass = current_relationship.try(:resource_klass)
50+
else
51+
warn "[RELATIONSHIP NOT FOUND] Relationship could not be found for #{current_path}."
52+
end
53+
54+
include_in_join = @force_eager_load || !current_relationship || current_relationship.eager_load_on_include
55+
56+
current[:include_related][fragment] ||= { include: false, include_related: {}, include_in_join: include_in_join }
4457
current = current[:include_related][fragment]
4558
end
4659
current
4760
end
4861

4962
def get_includes(directive)
50-
directive[:include_related].map do |name, directive|
63+
directive[:include_related].select { |k,v| v[:include_in_join] }.map do |name, directive|
5164
sub = get_includes(directive)
5265
sub.any? ? { name => sub } : name
5366
end

lib/jsonapi/relationship.rb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ module JSONAPI
22
class Relationship
33
attr_reader :acts_as_set, :foreign_key, :options, :name,
44
:class_name, :polymorphic, :always_include_linkage_data,
5-
:parent_resource
5+
:parent_resource, :eager_load_on_include
66

77
def initialize(name, options = {})
88
@name = name.to_s
@@ -13,6 +13,7 @@ def initialize(name, options = {})
1313
@relation_name = options.fetch(:relation_name, @name)
1414
@polymorphic = options.fetch(:polymorphic, false) == true
1515
@always_include_linkage_data = options.fetch(:always_include_linkage_data, false) == true
16+
@eager_load_on_include = options.fetch(:eager_load_on_include, true) == true
1617
end
1718

1819
alias_method :polymorphic?, :polymorphic

lib/jsonapi/request_parser.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -225,7 +225,7 @@ def parse_include_directives(include)
225225
include.push(unformat_key(included_resource).to_s)
226226
end
227227

228-
@include_directives = JSONAPI::IncludeDirectives.new(include)
228+
@include_directives = JSONAPI::IncludeDirectives.new(@resource_klass, include)
229229
end
230230

231231
def parse_filters(filters)

lib/jsonapi/resource.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -702,7 +702,7 @@ def apply_filters(records, filters, options = {})
702702
end
703703

704704
if required_includes.any?
705-
records = apply_includes(records, options.merge(include_directives: IncludeDirectives.new(required_includes)))
705+
records = apply_includes(records, options.merge(include_directives: IncludeDirectives.new(self, required_includes, force_eager_load: true)))
706706
end
707707

708708
records

lib/jsonapi/resource_serializer.rb

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,14 @@ class ResourceSerializer
1616
# serializer_options: additional options that will be passed to resource meta and links lambdas
1717

1818
def initialize(primary_resource_klass, options = {})
19-
@primary_class_name = primary_resource_klass._type
20-
@fields = options.fetch(:fields, {})
21-
@include = options.fetch(:include, [])
22-
@include_directives = options[:include_directives]
23-
@key_formatter = options.fetch(:key_formatter, JSONAPI.configuration.key_formatter)
24-
@id_formatter = ValueFormatter.value_formatter_for(:id)
25-
@link_builder = generate_link_builder(primary_resource_klass, options)
19+
@primary_resource_klass = primary_resource_klass
20+
@primary_class_name = primary_resource_klass._type
21+
@fields = options.fetch(:fields, {})
22+
@include = options.fetch(:include, [])
23+
@include_directives = options[:include_directives]
24+
@key_formatter = options.fetch(:key_formatter, JSONAPI.configuration.key_formatter)
25+
@id_formatter = ValueFormatter.value_formatter_for(:id)
26+
@link_builder = generate_link_builder(primary_resource_klass, options)
2627
@always_include_to_one_linkage_data = options.fetch(:always_include_to_one_linkage_data,
2728
JSONAPI.configuration.always_include_to_one_linkage_data)
2829
@always_include_to_many_linkage_data = options.fetch(:always_include_to_many_linkage_data,
@@ -41,7 +42,7 @@ def serialize_to_hash(source)
4142
is_resource_collection = source.respond_to?(:to_ary)
4243

4344
@included_objects = {}
44-
@include_directives ||= JSONAPI::IncludeDirectives.new(@include)
45+
@include_directives ||= JSONAPI::IncludeDirectives.new(@primary_resource_klass, @include)
4546

4647
process_primary(source, @include_directives.include_directives)
4748

test/fixtures/active_record.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -890,7 +890,7 @@ class PostResource < JSONAPI::Resource
890890

891891
has_one :author, class_name: 'Person'
892892
has_one :section
893-
has_many :tags, acts_as_set: true, inverse_relationship: :posts
893+
has_many :tags, acts_as_set: true, inverse_relationship: :posts, eager_load_on_include: false
894894
has_many :comments, acts_as_set: false, inverse_relationship: :post
895895

896896
# Not needed - just for testing

test/unit/serializer/include_directives_test.rb

Lines changed: 50 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -4,45 +4,49 @@
44
class IncludeDirectivesTest < ActiveSupport::TestCase
55

66
def test_one_level_one_include
7-
directives = JSONAPI::IncludeDirectives.new(['posts']).include_directives
7+
directives = JSONAPI::IncludeDirectives.new(PersonResource, ['posts']).include_directives
88

99
assert_hash_equals(
1010
{
1111
include_related: {
1212
posts: {
1313
include: true,
14-
include_related:{}
14+
include_related:{},
15+
include_in_join: true
1516
}
1617
}
1718
},
1819
directives)
1920
end
2021

2122
def test_one_level_multiple_includes
22-
directives = JSONAPI::IncludeDirectives.new(['posts', 'comments', 'tags']).include_directives
23+
directives = JSONAPI::IncludeDirectives.new(PersonResource, ['posts', 'comments', 'tags']).include_directives
2324

2425
assert_hash_equals(
2526
{
2627
include_related: {
2728
posts: {
2829
include: true,
29-
include_related:{}
30+
include_related:{},
31+
include_in_join: true
3032
},
3133
comments: {
3234
include: true,
33-
include_related:{}
35+
include_related:{},
36+
include_in_join: true
3437
},
3538
tags: {
3639
include: true,
37-
include_related:{}
40+
include_related:{},
41+
include_in_join: true
3842
}
3943
}
4044
},
4145
directives)
4246
end
4347

4448
def test_two_levels_include_full_path
45-
directives = JSONAPI::IncludeDirectives.new(['posts.comments']).include_directives
49+
directives = JSONAPI::IncludeDirectives.new(PersonResource, ['posts.comments']).include_directives
4650

4751
assert_hash_equals(
4852
{
@@ -52,17 +56,41 @@ def test_two_levels_include_full_path
5256
include_related:{
5357
comments: {
5458
include: true,
55-
include_related:{}
59+
include_related:{},
60+
include_in_join: true
5661
}
57-
}
62+
},
63+
include_in_join: true
64+
}
65+
}
66+
},
67+
directives)
68+
end
69+
70+
def test_no_eager_join
71+
directives = JSONAPI::IncludeDirectives.new(PersonResource, ['posts.tags']).include_directives
72+
73+
assert_hash_equals(
74+
{
75+
include_related: {
76+
posts: {
77+
include: true,
78+
include_related:{
79+
tags: {
80+
include: true,
81+
include_related:{},
82+
include_in_join: false
83+
}
84+
},
85+
include_in_join: true
5886
}
5987
}
6088
},
6189
directives)
6290
end
6391

6492
def test_two_levels_include_full_path_redundant
65-
directives = JSONAPI::IncludeDirectives.new(['posts','posts.comments']).include_directives
93+
directives = JSONAPI::IncludeDirectives.new(PersonResource, ['posts','posts.comments']).include_directives
6694

6795
assert_hash_equals(
6896
{
@@ -72,17 +100,19 @@ def test_two_levels_include_full_path_redundant
72100
include_related:{
73101
comments: {
74102
include: true,
75-
include_related:{}
103+
include_related:{},
104+
include_in_join: true
76105
}
77-
}
106+
},
107+
include_in_join: true
78108
}
79109
}
80110
},
81111
directives)
82112
end
83113

84114
def test_three_levels_include_full
85-
directives = JSONAPI::IncludeDirectives.new(['posts.comments.tags']).include_directives
115+
directives = JSONAPI::IncludeDirectives.new(PersonResource, ['posts.comments.tags']).include_directives
86116

87117
assert_hash_equals(
88118
{
@@ -95,19 +125,22 @@ def test_three_levels_include_full
95125
include_related:{
96126
tags: {
97127
include: true,
98-
include_related:{}
128+
include_related:{},
129+
include_in_join: true
99130
}
100-
}
131+
},
132+
include_in_join: true
101133
}
102-
}
134+
},
135+
include_in_join: true
103136
}
104137
}
105138
},
106139
directives)
107140
end
108141

109142
def test_three_levels_include_full_model_includes
110-
directives = JSONAPI::IncludeDirectives.new(['posts.comments.tags'])
143+
directives = JSONAPI::IncludeDirectives.new(PersonResource, ['posts.comments.tags'])
111144
assert_array_equals([{:posts=>[{:comments=>[:tags]}]}], directives.model_includes)
112145
end
113146
end

0 commit comments

Comments
 (0)