Skip to content

Commit 43f60a3

Browse files
authored
fix: handle instance dependent associations (#567)
1 parent d6d381d commit 43f60a3

File tree

11 files changed

+71
-25
lines changed

11 files changed

+71
-25
lines changed

app/services/forest_liana/base_getter.rb

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,5 +29,27 @@ def includes_for_serialization
2929
def compute_includes
3030
@includes = ForestLiana::QueryHelper.get_one_association_names_symbol(@resource)
3131
end
32+
33+
def optimize_record_loading(resource, records)
34+
instance_dependent_associations = instance_dependent_associations(resource)
35+
eager_loads = @includes - instance_dependent_associations
36+
37+
result = records.eager_load(eager_loads)
38+
39+
# Rails 7 can mix `eager_load` and `preload` in the same scope
40+
# Rails 6 cannot mix `eager_load` and `preload` in the same scope
41+
# Rails 6 and 7 cannot mix `eager_load` and `includes` in the same scope
42+
if Rails::VERSION::MAJOR >= 7
43+
result = result.preload(instance_dependent_associations)
44+
end
45+
46+
result
47+
end
48+
49+
def instance_dependent_associations(resource)
50+
@includes.select do |association_name|
51+
resource.reflect_on_association(association_name)&.scope&.arity&.positive?
52+
end
53+
end
3254
end
3355
end

app/services/forest_liana/has_many_getter.rb

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -64,12 +64,8 @@ def model_association
6464
end
6565

6666
def prepare_query
67-
@records = @search_query_builder.perform(
68-
get_resource()
69-
.find(@params[:id])
70-
.send(@params[:association_name])
71-
.eager_load(@includes)
72-
)
67+
association = get_resource().find(@params[:id]).send(@params[:association_name])
68+
@records = optimize_record_loading(association, @search_query_builder.perform(association))
7369
end
7470

7571
def offset

app/services/forest_liana/pie_stat_getter.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ class PieStatGetter < StatGetter
55
def perform
66
if @params[:group_by_field]
77
timezone_offset = @params[:timezone].to_i
8-
resource = get_resource().eager_load(@includes)
8+
resource = optimize_record_loading(@resource, get_resource)
99

1010
filters = ForestLiana::ScopeManager.append_scope_for_user(@params[:filters], @user, @resource.name)
1111

app/services/forest_liana/resource_getter.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ def initialize(resource, params, forest_user)
1212
end
1313

1414
def perform
15-
records = get_resource().eager_load(@includes)
15+
records = optimize_record_loading(@resource, get_resource())
1616
scoped_records = ForestLiana::ScopeManager.apply_scopes_on_records(records, @user, @collection_name, @params[:timezone])
1717
@record = scoped_records.find(@params[:id])
1818
end

app/services/forest_liana/resources_getter.rb

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -66,13 +66,13 @@ def self.get_ids_from_request(params, user)
6666
end
6767

6868
def perform
69-
@records = @records.eager_load(@includes)
69+
@records = optimize_record_loading(@resource, @records)
7070
end
7171

7272
def count
73-
# NOTICE: For performance reasons, do not eager load the data if there is no search or
73+
# NOTICE: For performance reasons, do not optimize loading the data if there is no search or
7474
# filters on associations.
75-
@records_count = @count_needs_includes ? @records.eager_load(@includes).count : @records.count
75+
@records_count = @count_needs_includes ? optimize_record_loading(@resource, @records).count : @records.count
7676
end
7777

7878
def query_for_batch

app/services/forest_liana/value_stat_getter.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ class ValueStatGetter < StatGetter
44

55
def perform
66
return if @params[:aggregate].blank?
7-
resource = get_resource().eager_load(@includes)
7+
resource = optimize_record_loading(@resource, get_resource)
88

99
filters = ForestLiana::ScopeManager.append_scope_for_user(@params[:filters], @user, @resource.name)
1010

spec/dummy/app/models/island.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,5 @@ class Island < ActiveRecord::Base
33

44
has_many :trees
55
has_one :location
6+
has_one :eponymous_tree, ->(record) { where(name: record.name) }, class_name: 'Tree'
67
end

spec/dummy/app/models/tree.rb

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,9 @@ class Tree < ActiveRecord::Base
22
belongs_to :owner, class_name: 'User', inverse_of: :trees_owned
33
belongs_to :cutter, class_name: 'User', inverse_of: :trees_cut
44
belongs_to :island
5+
belongs_to :eponymous_island,
6+
->(record) { where(name: record.name) },
7+
class_name: 'Island',
8+
inverse_of: :eponymous_tree,
9+
optional: true
510
end

spec/helpers/forest_liana/query_helper_spec.rb

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -14,29 +14,35 @@ module ForestLiana
1414
end
1515
end
1616

17-
context 'on a model having 3 belongsTo associations' do
17+
context 'on a model having some belongsTo associations' do
18+
let(:expected_association_attributes) do
19+
[
20+
{ name: :owner, klass: User },
21+
{ name: :cutter, klass: User },
22+
{ name: :island, klass: Island },
23+
{ name: :eponymous_island, klass: Island },
24+
]
25+
end
26+
1827
it 'should return the one-one associations' do
1928
associations = QueryHelper.get_one_associations(Tree)
20-
expect(associations.length).to eq(3)
21-
expect(associations.first.name).to eq(:owner)
22-
expect(associations.first.klass).to eq(User)
23-
expect(associations.second.name).to eq(:cutter)
24-
expect(associations.second.klass).to eq(User)
25-
expect(associations.third.name).to eq(:island)
26-
expect(associations.third.klass).to eq(Island)
29+
expect(associations.length).to eq(expected_association_attributes.length)
30+
associations.zip(expected_association_attributes).each do |association, expected_attributes|
31+
expect(association).to have_attributes(expected_attributes)
32+
end
2733
end
2834
end
2935
end
3036

3137
describe 'get_one_association_names_symbol' do
3238
it 'should return the one-one associations names as symbols' do
33-
expect(QueryHelper.get_one_association_names_symbol(Tree)).to eq([:owner, :cutter, :island])
39+
expect(QueryHelper.get_one_association_names_symbol(Tree)).to eq([:owner, :cutter, :island, :eponymous_island])
3440
end
3541
end
3642

3743
describe 'get_one_association_names_string' do
3844
it 'should return the one-one associations names as strings' do
39-
expect(QueryHelper.get_one_association_names_string(Tree)).to eq(['owner', 'cutter', 'island'])
45+
expect(QueryHelper.get_one_association_names_string(Tree)).to eq(['owner', 'cutter', 'island', 'eponymous_island'])
4046
end
4147
end
4248

@@ -64,8 +70,9 @@ module ForestLiana
6470
end
6571

6672
it 'should return relationships on models having a custom table name' do
67-
expect(tables_associated_to_relations_name['isle'].length).to eq(1)
73+
expect(tables_associated_to_relations_name['isle'].length).to eq(2)
6874
expect(tables_associated_to_relations_name['isle'].first).to eq(:island)
75+
expect(tables_associated_to_relations_name['isle'].second).to eq(:eponymous_island)
6976
end
7077
end
7178
end

spec/services/forest_liana/resources_getter_spec.rb

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,21 @@ def init_scopes
155155
end
156156
end
157157

158+
describe 'when getting instance dependent associations' do
159+
let(:resource) { Island }
160+
let(:fields) { { 'Island' => 'id,eponymous_tree', 'eponymous_tree' => 'id,name'} }
161+
162+
it 'should get only the expected records' do
163+
getter.perform
164+
records = getter.records
165+
count = getter.count
166+
167+
expect(records.count).to eq Island.count
168+
expect(count).to eq Island.count
169+
expect(records.map(&:name)).to match_array(Island.pluck(:name))
170+
end
171+
end
172+
158173
describe 'when filtering on an ambiguous field' do
159174
let(:resource) { Tree }
160175
let(:pageSize) { 5 }

0 commit comments

Comments
 (0)