aboutsummaryrefslogtreecommitdiffstats
path: root/activerecord/lib/active_record/associations/preloader/through_association.rb
diff options
context:
space:
mode:
Diffstat (limited to 'activerecord/lib/active_record/associations/preloader/through_association.rb')
-rw-r--r--activerecord/lib/active_record/associations/preloader/through_association.rb157
1 files changed, 82 insertions, 75 deletions
diff --git a/activerecord/lib/active_record/associations/preloader/through_association.rb b/activerecord/lib/active_record/associations/preloader/through_association.rb
index b0203909ce..bec1c4c94a 100644
--- a/activerecord/lib/active_record/associations/preloader/through_association.rb
+++ b/activerecord/lib/active_record/associations/preloader/through_association.rb
@@ -1,108 +1,115 @@
+# frozen_string_literal: true
+
module ActiveRecord
module Associations
class Preloader
- module ThroughAssociation #:nodoc:
- def through_reflection
- reflection.through_reflection
+ class ThroughAssociation < Association # :nodoc:
+ PRELOADER = ActiveRecord::Associations::Preloader.new
+
+ def initialize(*)
+ super
+ @already_loaded = owners.first.association(through_reflection.name).loaded?
end
- def source_reflection
- reflection.source_reflection
+ def preloaded_records
+ @preloaded_records ||= source_preloaders.flat_map(&:preloaded_records)
end
- def associated_records_by_owner(preloader)
- preloader.preload(owners,
- through_reflection.name,
- through_scope)
+ def records_by_owner
+ return @records_by_owner if defined?(@records_by_owner)
+ source_records_by_owner = source_preloaders.map(&:records_by_owner).reduce(:merge)
+ through_records_by_owner = through_preloaders.map(&:records_by_owner).reduce(:merge)
- through_records = owners.map do |owner|
- association = owner.association through_reflection.name
+ @records_by_owner = owners.each_with_object({}) do |owner, result|
+ through_records = through_records_by_owner[owner] || []
- center = target_records_from_association(association)
- [owner, Array(center)]
- end
-
- reset_association owners, through_reflection.name
+ if @already_loaded
+ if source_type = reflection.options[:source_type]
+ through_records = through_records.select do |record|
+ record[reflection.foreign_type] == source_type
+ end
+ end
+ end
- middle_records = through_records.flat_map { |(_,rec)| rec }
+ records = through_records.flat_map do |record|
+ source_records_by_owner[record]
+ end
- preloaders = preloader.preload(middle_records,
- source_reflection.name,
- reflection_scope)
+ records.compact!
+ records.sort_by! { |rhs| preload_index[rhs] } if scope.order_values.any?
+ records.uniq! if scope.distinct_value
+ result[owner] = records
+ end
+ end
- @preloaded_records = preloaders.flat_map(&:preloaded_records)
+ private
+ def source_preloaders
+ @source_preloaders ||= PRELOADER.preload(middle_records, source_reflection.name, scope)
+ end
- middle_to_pl = preloaders.each_with_object({}) do |pl,h|
- pl.owners.each { |middle|
- h[middle] = pl
- }
+ def middle_records
+ through_preloaders.flat_map(&:preloaded_records)
end
- through_records.each_with_object({}) do |(lhs,center), records_by_owner|
- pl_to_middle = center.group_by { |record| middle_to_pl[record] }
+ def through_preloaders
+ @through_preloaders ||= PRELOADER.preload(owners, through_reflection.name, through_scope)
+ end
- records_by_owner[lhs] = pl_to_middle.flat_map do |pl, middles|
- rhs_records = middles.flat_map { |r|
- association = r.association source_reflection.name
+ def through_reflection
+ reflection.through_reflection
+ end
- target_records_from_association(association)
- }.compact
+ def source_reflection
+ reflection.source_reflection
+ end
- # Respect the order on `reflection_scope` if it exists, else use the natural order.
- if reflection_scope.values[:order].present?
- @id_map ||= id_to_index_map @preloaded_records
- rhs_records.sort_by { |rhs| @id_map[rhs] }
- else
- rhs_records
- end
+ def preload_index
+ @preload_index ||= preloaded_records.each_with_object({}).with_index do |(record, result), index|
+ result[record] = index
end
end
- end
- private
+ def through_scope
+ scope = through_reflection.klass.unscoped
+ options = reflection.options
- def id_to_index_map(ids)
- id_map = {}
- ids.each_with_index { |id, index| id_map[id] = index }
- id_map
- end
+ values = reflection_scope.values
+ if annotations = values[:annotate]
+ scope.annotate!(*annotations)
+ end
- def reset_association(owners, association_name)
- should_reset = (through_scope != through_reflection.klass.unscoped) ||
- (reflection.options[:source_type] && through_reflection.collection?)
+ if options[:source_type]
+ scope.where! reflection.foreign_type => options[:source_type]
+ elsif !reflection_scope.where_clause.empty?
+ scope.where_clause = reflection_scope.where_clause
- # Don't cache the association - we would only be caching a subset
- if should_reset
- owners.each { |owner|
- owner.association(association_name).reset
- }
- end
- end
+ if includes = values[:includes]
+ scope.includes!(source_reflection.name => includes)
+ else
+ scope.includes!(source_reflection.name)
+ end
+
+ if values[:references] && !values[:references].empty?
+ scope.references!(values[:references])
+ else
+ scope.references!(source_reflection.table_name)
+ end
+ if joins = values[:joins]
+ scope.joins!(source_reflection.name => joins)
+ end
- def through_scope
- scope = through_reflection.klass.unscoped
+ if left_outer_joins = values[:left_outer_joins]
+ scope.left_outer_joins!(source_reflection.name => left_outer_joins)
+ end
- if options[:source_type]
- scope.where! reflection.foreign_type => options[:source_type]
- else
- unless reflection_scope.where_clause.empty?
- scope.includes_values = Array(reflection_scope.values[:includes] || options[:source])
- scope.where_clause = reflection_scope.where_clause
+ if scope.eager_loading? && order_values = values[:order]
+ scope = scope.order(order_values)
+ end
end
- scope.references! reflection_scope.values[:references]
- if scope.eager_loading? && order_values = reflection_scope.values[:order]
- scope = scope.order(order_values)
- end
+ scope
end
-
- scope
- end
-
- def target_records_from_association(association)
- association.loaded? ? association.target : association.reader
- end
end
end
end