diff options
Diffstat (limited to 'activerecord/lib/active_record/associations/preloader')
9 files changed, 173 insertions, 305 deletions
diff --git a/activerecord/lib/active_record/associations/preloader/association.rb b/activerecord/lib/active_record/associations/preloader/association.rb index 3032bc786e..46532f651e 100644 --- a/activerecord/lib/active_record/associations/preloader/association.rb +++ b/activerecord/lib/active_record/associations/preloader/association.rb @@ -1,159 +1,136 @@ +# frozen_string_literal: true + module ActiveRecord module Associations class Preloader class Association #:nodoc: - attr_reader :owners, :reflection, :preload_scope, :model, :klass - attr_reader :preloaded_records - def initialize(klass, owners, reflection, preload_scope) @klass = klass @owners = owners @reflection = reflection @preload_scope = preload_scope @model = owners.first && owners.first.class - @scope = nil - @preloaded_records = [] - end - - def run(preloader) - preload(preloader) - end - - def preload(preloader) - raise NotImplementedError - end - - def scope - @scope ||= build_scope - end - - def records_for(ids) - query_scope(ids) - end - - def query_scope(ids) - scope.where(association_key_name => ids) - end - - def table - klass.arel_table - end - - # The name of the key on the associated records - def association_key_name - raise NotImplementedError - end - - # This is overridden by HABTM as the condition should be on the foreign_key column in - # the join table - def association_key - klass.arel_attribute(association_key_name, table) - end - - # The name of the key on the model which declares the association - def owner_key_name - raise NotImplementedError - end - - def options - reflection.options end - private - - def associated_records_by_owner(preloader) - records = load_records - owners.each_with_object({}) do |owner, result| - result[owner] = records[convert_key(owner[owner_key_name])] || [] + def run + if !preload_scope || preload_scope.empty_scope? + owners.each do |owner| + associate_records_to_owner(owner, records_by_owner[owner] || []) + end + else + # Custom preload scope is used and + # the association can not be marked as loaded + # Loading into a Hash instead + records_by_owner end + self end - def owner_keys - unless defined?(@owner_keys) - @owner_keys = owners.map do |owner| - owner[owner_key_name] + def records_by_owner + @records_by_owner ||= preloaded_records.each_with_object({}) do |record, result| + owners_by_key[convert_key(record[association_key_name])].each do |owner| + (result[owner] ||= []) << record end - @owner_keys.uniq! - @owner_keys.compact! end - @owner_keys end - def key_conversion_required? - @key_conversion_required ||= association_key_type != owner_key_type - end - - def convert_key(key) - if key_conversion_required? - key.to_s - else - key + def preloaded_records + return @preloaded_records if defined?(@preloaded_records) + return [] if owner_keys.empty? + # Some databases impose a limit on the number of ids in a list (in Oracle it's 1000) + # Make several smaller queries if necessary or make one query if the adapter supports it + slices = owner_keys.each_slice(klass.connection.in_clause_length || owner_keys.size) + @preloaded_records = slices.flat_map do |slice| + records_for(slice) end end - def association_key_type - @klass.type_for_attribute(association_key_name.to_s).type - end + private + attr_reader :owners, :reflection, :preload_scope, :model, :klass - def owner_key_type - @model.type_for_attribute(owner_key_name.to_s).type - end + # The name of the key on the associated records + def association_key_name + reflection.join_primary_key(klass) + end - def load_records - return {} if owner_keys.empty? - # Some databases impose a limit on the number of ids in a list (in Oracle it's 1000) - # Make several smaller queries if necessary or make one query if the adapter supports it - slices = owner_keys.each_slice(klass.connection.in_clause_length || owner_keys.size) - @preloaded_records = slices.flat_map do |slice| - records_for(slice) + # The name of the key on the model which declares the association + def owner_key_name + reflection.join_foreign_key end - @preloaded_records.group_by do |record| - convert_key(record[association_key_name]) + + def associate_records_to_owner(owner, records) + association = owner.association(reflection.name) + if reflection.collection? + association.target = records + else + association.target = records.first + end end - end - def reflection_scope - @reflection_scope ||= reflection.scope ? klass.unscoped.instance_exec(nil, &reflection.scope) : klass.unscoped - end + def owner_keys + @owner_keys ||= owners_by_key.keys + end - def build_scope - scope = klass.unscoped + def owners_by_key + @owners_by_key ||= owners.each_with_object({}) do |owner, result| + key = convert_key(owner[owner_key_name]) + (result[key] ||= []) << owner if key + end + end - values = reflection_scope.values - preload_values = preload_scope.values + def key_conversion_required? + unless defined?(@key_conversion_required) + @key_conversion_required = (association_key_type != owner_key_type) + end - scope.where_clause = reflection_scope.where_clause + preload_scope.where_clause - scope.references_values = Array(values[:references]) + Array(preload_values[:references]) + @key_conversion_required + end - if preload_values[:select] || values[:select] - scope._select!(preload_values[:select] || values[:select]) + def convert_key(key) + if key_conversion_required? + key.to_s + else + key + end end - scope.includes! preload_values[:includes] || values[:includes] - if preload_scope.joins_values.any? - scope.joins!(preload_scope.joins_values) - else - scope.joins!(reflection_scope.joins_values) + + def association_key_type + @klass.type_for_attribute(association_key_name).type end - if order_values = preload_values[:order] || values[:order] - scope.order!(order_values) + def owner_key_type + @model.type_for_attribute(owner_key_name).type end - if preload_values[:reordering] || values[:reordering] - scope.reordering_value = true + def records_for(ids) + scope.where(association_key_name => ids).load do |record| + # Processing only the first owner + # because the record is modified but not an owner + owner = owners_by_key[convert_key(record[association_key_name])].first + association = owner.association(reflection.name) + association.set_inverse_instance(record) + end end - if preload_values[:readonly] || values[:readonly] - scope.readonly! + def scope + @scope ||= build_scope end - if options[:as] - scope.where!(klass.table_name => { reflection.type => model.base_class.sti_name }) + def reflection_scope + @reflection_scope ||= reflection.scope ? reflection.scope_for(klass.unscoped) : klass.unscoped end - scope.unscope_values = Array(values[:unscope]) + Array(preload_values[:unscope]) - klass.default_scoped.merge(scope) - end + def build_scope + scope = klass.scope_for_association + + if reflection.type && !reflection.through_reflection? + scope.where!(reflection.type => model.polymorphic_name) + end + + scope.merge!(reflection_scope) if reflection.scope + scope.merge!(preload_scope) if preload_scope + scope + end end end end diff --git a/activerecord/lib/active_record/associations/preloader/belongs_to.rb b/activerecord/lib/active_record/associations/preloader/belongs_to.rb deleted file mode 100644 index 5091d4717a..0000000000 --- a/activerecord/lib/active_record/associations/preloader/belongs_to.rb +++ /dev/null @@ -1,17 +0,0 @@ -module ActiveRecord - module Associations - class Preloader - class BelongsTo < SingularAssociation #:nodoc: - - def association_key_name - reflection.options[:primary_key] || klass && klass.primary_key - end - - def owner_key_name - reflection.foreign_key - end - - end - end - end -end diff --git a/activerecord/lib/active_record/associations/preloader/collection_association.rb b/activerecord/lib/active_record/associations/preloader/collection_association.rb deleted file mode 100644 index 9939280fa4..0000000000 --- a/activerecord/lib/active_record/associations/preloader/collection_association.rb +++ /dev/null @@ -1,18 +0,0 @@ -module ActiveRecord - module Associations - class Preloader - class CollectionAssociation < Association #:nodoc: - private - - def preload(preloader) - associated_records_by_owner(preloader).each do |owner, records| - association = owner.association(reflection.name) - association.loaded! - association.target.concat(records) - records.each { |record| association.set_inverse_instance(record) } - end - end - end - end - end -end diff --git a/activerecord/lib/active_record/associations/preloader/has_many.rb b/activerecord/lib/active_record/associations/preloader/has_many.rb deleted file mode 100644 index 3ea91a8c11..0000000000 --- a/activerecord/lib/active_record/associations/preloader/has_many.rb +++ /dev/null @@ -1,17 +0,0 @@ -module ActiveRecord - module Associations - class Preloader - class HasMany < CollectionAssociation #:nodoc: - - def association_key_name - reflection.foreign_key - end - - def owner_key_name - reflection.active_record_primary_key - end - - end - end - end -end diff --git a/activerecord/lib/active_record/associations/preloader/has_many_through.rb b/activerecord/lib/active_record/associations/preloader/has_many_through.rb deleted file mode 100644 index 2029871f39..0000000000 --- a/activerecord/lib/active_record/associations/preloader/has_many_through.rb +++ /dev/null @@ -1,19 +0,0 @@ -module ActiveRecord - module Associations - class Preloader - class HasManyThrough < CollectionAssociation #:nodoc: - include ThroughAssociation - - def associated_records_by_owner(preloader) - records_by_owner = super - - if reflection_scope.distinct_value - records_by_owner.each_value(&:uniq!) - end - - records_by_owner - end - end - end - end -end diff --git a/activerecord/lib/active_record/associations/preloader/has_one.rb b/activerecord/lib/active_record/associations/preloader/has_one.rb deleted file mode 100644 index c4add621ca..0000000000 --- a/activerecord/lib/active_record/associations/preloader/has_one.rb +++ /dev/null @@ -1,15 +0,0 @@ -module ActiveRecord - module Associations - class Preloader - class HasOne < SingularAssociation #:nodoc: - def association_key_name - reflection.foreign_key - end - - def owner_key_name - reflection.active_record_primary_key - end - end - end - end -end diff --git a/activerecord/lib/active_record/associations/preloader/has_one_through.rb b/activerecord/lib/active_record/associations/preloader/has_one_through.rb deleted file mode 100644 index f063f85574..0000000000 --- a/activerecord/lib/active_record/associations/preloader/has_one_through.rb +++ /dev/null @@ -1,9 +0,0 @@ -module ActiveRecord - module Associations - class Preloader - class HasOneThrough < SingularAssociation #:nodoc: - include ThroughAssociation - end - end - end -end diff --git a/activerecord/lib/active_record/associations/preloader/singular_association.rb b/activerecord/lib/active_record/associations/preloader/singular_association.rb deleted file mode 100644 index f60647a81e..0000000000 --- a/activerecord/lib/active_record/associations/preloader/singular_association.rb +++ /dev/null @@ -1,21 +0,0 @@ -module ActiveRecord - module Associations - class Preloader - class SingularAssociation < Association #:nodoc: - - private - - def preload(preloader) - associated_records_by_owner(preloader).each do |owner, associated_records| - record = associated_records.first - - association = owner.association(reflection.name) - association.target = record - association.set_inverse_instance(record) if record - end - end - - end - end - end -end 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 |