Skip to content

Commit 2491f66

Browse files
feat(active-record): improve performance when a belongsTo relation is included (#729)
1 parent c85bad3 commit 2491f66

File tree

4 files changed

+114
-3
lines changed

4 files changed

+114
-3
lines changed

app/services/forest_liana/resources_getter.rb

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,20 @@ def self.get_ids_from_request(params, user)
3030
end
3131

3232
def perform
33-
@records = optimize_record_loading(@resource, @records)
33+
polymorphic_association, preload_loads = analyze_associations(@resource)
34+
includes = @includes.uniq - polymorphic_association - preload_loads
35+
has_smart_fields = @params[:fields][@collection_name].split(',').any? do |field|
36+
ForestLiana::SchemaHelper.is_smart_field?(@resource, field)
37+
end
38+
39+
if includes.empty? || has_smart_fields
40+
@records = optimize_record_loading(@resource, @records)
41+
else
42+
select = compute_select_fields
43+
@records = optimize_record_loading(@resource, @records).references(includes).select(*select)
44+
end
45+
46+
@records
3447
end
3548

3649
def count
@@ -210,5 +223,29 @@ def limit
210223
def pagination?
211224
@params[:page]&.dig(:number)
212225
end
226+
227+
def compute_select_fields
228+
select = ['_forest_admin_eager_load']
229+
@params[:fields][@collection_name].split(',').each do |path|
230+
if @params[:fields].key?(path)
231+
association = ForestLiana::QueryHelper.get_one_associations(@resource)
232+
.select { |association| association.name == path.to_sym }
233+
.first
234+
table_name = association.table_name
235+
236+
@params[:fields][path].split(',').each do |association_path|
237+
if ForestLiana::SchemaHelper.is_smart_field?(association.klass, association_path)
238+
association.klass.attribute_names.each { |attribute| select << "#{table_name}.#{attribute}" }
239+
else
240+
select << "#{table_name}.#{association_path}"
241+
end
242+
end
243+
else
244+
select << "#{@resource.table_name}.#{path}"
245+
end
246+
end
247+
248+
select
249+
end
213250
end
214251
end
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
module ForestLiana
2+
module ActiveRecordOverride
3+
module Associations
4+
require 'active_record/associations/join_dependency'
5+
module JoinDependency
6+
def apply_column_aliases(relation)
7+
if !(@join_root_alias = relation.select_values.empty?) &&
8+
relation.select_values.first.to_s == '_forest_admin_eager_load'
9+
10+
relation.select_values.shift
11+
used_cols = {}
12+
# Find and expand out all column names being used in select(...)
13+
new_select_values = relation.select_values.map(&:to_s).each_with_object([]) do |col, select|
14+
unless col.include?(' ') # Pass it through if it's some expression (No chance for a simple column reference)
15+
col = if (col_parts = col.split('.')).length == 1
16+
[col]
17+
else
18+
[col_parts[0..-2].join('.'), col_parts.last]
19+
end
20+
used_cols[col] = nil
21+
end
22+
select << col
23+
end
24+
25+
if new_select_values.present?
26+
relation.select_values = new_select_values
27+
else
28+
relation.select_values.clear
29+
end
30+
31+
@aliases ||= ActiveRecord::Associations::JoinDependency::Aliases.new(join_root.each_with_index.map do |join_part, i|
32+
join_alias = join_part.table&.table_alias || join_part.table_name
33+
keys = [join_part.base_klass.primary_key] # Always include the primary key
34+
35+
# # %%% Optional to include all foreign keys:
36+
# keys.concat(join_part.base_klass.reflect_on_all_associations.select { |a| a.belongs_to? }.map(&:foreign_key))
37+
# Add foreign keys out to referenced tables that we belongs_to
38+
join_part.children.each { |child| keys << child.reflection.foreign_key if child.reflection.belongs_to? }
39+
40+
# Add the foreign key that got us here -- "the train we rode in on" -- if we arrived from
41+
# a has_many or has_one:
42+
if join_part.is_a?(ActiveRecord::Associations::JoinDependency::JoinAssociation) &&
43+
!join_part.reflection.belongs_to?
44+
keys << join_part.reflection.foreign_key
45+
end
46+
keys = keys.compact # In case we're using composite_primary_keys
47+
j = 0
48+
columns = join_part.column_names.each_with_object([]) do |column_name, s|
49+
# Include columns chosen in select(...) as well as the PK and any relevant FKs
50+
if used_cols.keys.find { |c| (c.length == 1 || c.first == join_alias) && c.last == column_name } ||
51+
keys.find { |c| c == column_name }
52+
s << ActiveRecord::Associations::JoinDependency::Aliases::Column.new(column_name, "t#{i}_r#{j}")
53+
end
54+
j += 1
55+
end
56+
ActiveRecord::Associations::JoinDependency::Aliases::Table.new(join_part, columns)
57+
end)
58+
relation.select_values.clear
59+
end
60+
relation._select!(-> { aliases.columns })
61+
end
62+
end
63+
end
64+
end
65+
end

lib/forest_liana/engine.rb

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
require 'bcrypt'
88
require_relative 'bootstrapper'
99
require_relative 'collection'
10+
require_relative 'active_record_override'
1011

1112
module Rack
1213
class Cors
@@ -90,6 +91,12 @@ def eager_load_active_record_descendants app
9091
end
9192
end
9293

94+
initializer 'forest_liana.override_active_record_dependency' do
95+
ActiveSupport.on_load(:active_record) do
96+
ActiveRecord::Associations::JoinDependency.prepend(ForestLiana::ActiveRecordOverride::Associations::JoinDependency)
97+
end
98+
end
99+
93100
config.after_initialize do |app|
94101
if error
95102
FOREST_REPORTER.report error

spec/services/forest_liana/resources_getter_spec.rb

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ module ForestLiana
44
let(:pageSize) { 10 }
55
let(:pageNumber) { 1 }
66
let(:sort) { 'id' }
7-
let(:fields) {}
7+
let(:fields) { { resource.name => 'id' } }
88
let(:filters) {}
99
let(:scopes) { {'scopes' => {}, 'team' => {'id' => '1', 'name' => 'Operations'}} }
1010
let(:rendering_id) { 13 }
@@ -190,6 +190,7 @@ def association_connection.current_database
190190

191191
describe 'when on a model having a reserved SQL word as name' do
192192
let(:resource) { Reference }
193+
let(:fields) { { resource.name => 'id' } }
193194

194195
it 'should get the ressource properly' do
195196
getter.perform
@@ -219,6 +220,7 @@ def association_connection.current_database
219220

220221
describe 'when sorting by a belongs_to association' do
221222
let(:resource) { Tree }
223+
let(:fields) { { resource.name => 'id' } }
222224
let(:sort) { 'owner.name' }
223225

224226
it 'should get only the expected records' do
@@ -501,7 +503,7 @@ def association_connection.current_database
501503
describe 'when scopes are defined' do
502504
let(:resource) { Island }
503505
let(:pageSize) { 15 }
504-
let(:fields) { }
506+
let(:fields) { { resource.name => 'id' } }
505507
let(:filters) { }
506508
let(:scopes) {
507509
{

0 commit comments

Comments
 (0)