aboutsummaryrefslogtreecommitdiffstats
path: root/activerecord/lib/active_record/associations/preloader
diff options
context:
space:
mode:
Diffstat (limited to 'activerecord/lib/active_record/associations/preloader')
-rw-r--r--activerecord/lib/active_record/associations/preloader/association.rb146
-rw-r--r--activerecord/lib/active_record/associations/preloader/belongs_to.rb15
-rw-r--r--activerecord/lib/active_record/associations/preloader/collection_association.rb17
-rw-r--r--activerecord/lib/active_record/associations/preloader/has_many.rb15
-rw-r--r--activerecord/lib/active_record/associations/preloader/has_many_through.rb19
-rw-r--r--activerecord/lib/active_record/associations/preloader/has_one.rb15
-rw-r--r--activerecord/lib/active_record/associations/preloader/has_one_through.rb9
-rw-r--r--activerecord/lib/active_record/associations/preloader/singular_association.rb18
-rw-r--r--activerecord/lib/active_record/associations/preloader/through_association.rb145
9 files changed, 125 insertions, 274 deletions
diff --git a/activerecord/lib/active_record/associations/preloader/association.rb b/activerecord/lib/active_record/associations/preloader/association.rb
index 4072d19380..d6f7359055 100644
--- a/activerecord/lib/active_record/associations/preloader/association.rb
+++ b/activerecord/lib/active_record/associations/preloader/association.rb
@@ -1,8 +1,9 @@
+# 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)
@@ -11,86 +12,64 @@ module ActiveRecord
@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)
- 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
+ records = load_records do |record|
+ owner = owners_by_key[convert_key(record[association_key_name])]
+ association = owner.association(reflection.name)
+ association.set_inverse_instance(record)
+ end
- def options
- reflection.options
+ owners.each do |owner|
+ associate_records_to_owner(owner, records[convert_key(owner[owner_key_name])] || [])
+ end
end
private
+ attr_reader :owners, :reflection, :preload_scope, :model, :klass
- def associated_records_by_owner(preloader)
- records = load_records do |record|
- owner = owners_by_key[convert_key(record[association_key_name])]
- association = owner.association(reflection.name)
- association.set_inverse_instance(record)
- end
+ # The name of the key on the associated records
+ def association_key_name
+ reflection.join_primary_key(klass)
+ end
+
+ # The name of the key on the model which declares the association
+ def owner_key_name
+ reflection.join_foreign_key
+ end
- owners.each_with_object({}) do |owner, result|
- result[owner] = records[convert_key(owner[owner_key_name])] || []
+ def associate_records_to_owner(owner, records)
+ association = owner.association(reflection.name)
+ association.loaded!
+ if reflection.collection?
+ association.target.concat(records)
+ else
+ association.target = records.first unless records.empty?
end
end
def owner_keys
- unless defined?(@owner_keys)
- @owner_keys = owners.map do |owner|
- owner[owner_key_name]
- end
- @owner_keys.uniq!
- @owner_keys.compact!
- end
- @owner_keys
+ @owner_keys ||= owners_by_key.keys
end
def owners_by_key
unless defined?(@owners_by_key)
@owners_by_key = owners.each_with_object({}) do |owner, h|
- h[convert_key(owner[owner_key_name])] = owner
+ key = convert_key(owner[owner_key_name])
+ h[key] = owner if key
end
end
@owners_by_key
end
def key_conversion_required?
- @key_conversion_required ||= association_key_type != owner_key_type
+ unless defined?(@key_conversion_required)
+ @key_conversion_required = (association_key_type != owner_key_type)
+ end
+
+ @key_conversion_required
end
def convert_key(key)
@@ -102,11 +81,11 @@ module ActiveRecord
end
def association_key_type
- @klass.type_for_attribute(association_key_name.to_s).type
+ @klass.type_for_attribute(association_key_name).type
end
def owner_key_type
- @model.type_for_attribute(owner_key_name.to_s).type
+ @model.type_for_attribute(owner_key_name).type
end
def load_records(&block)
@@ -115,54 +94,35 @@ module ActiveRecord
# 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).load(&block)
+ records_for(slice, &block)
end
@preloaded_records.group_by do |record|
convert_key(record[association_key_name])
end
end
- def reflection_scope
- @reflection_scope ||= reflection.scope_for(klass)
+ def records_for(ids, &block)
+ scope.where(association_key_name => ids).load(&block)
end
- def build_scope
- scope = klass.unscoped
-
- values = reflection_scope.values
- preload_values = preload_scope.values
-
- scope.where_clause = reflection_scope.where_clause + preload_scope.where_clause
- scope.references_values = Array(values[:references]) + Array(preload_values[:references])
-
- if preload_values[:select] || values[:select]
- scope._select!(preload_values[:select] || values[:select])
- 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)
- end
-
- if order_values = preload_values[:order] || values[:order]
- scope.order!(order_values)
- end
+ def scope
+ @scope ||= build_scope
+ end
- if preload_values[:reordering] || values[:reordering]
- scope.reordering_value = true
- end
+ def reflection_scope
+ @reflection_scope ||= reflection.scope ? reflection.scope_for(klass.unscoped) : klass.unscoped
+ end
- if preload_values[:readonly] || values[:readonly]
- scope.readonly!
- end
+ def build_scope
+ scope = klass.scope_for_association
- if options[:as]
- scope.where!(klass.table_name => { reflection.type => model.base_class.sti_name })
+ if reflection.type
+ scope.where!(reflection.type => model.polymorphic_name)
end
- scope.unscope_values = Array(values[:unscope]) + Array(preload_values[:unscope])
- klass.default_scoped.merge(scope)
+ scope.merge!(reflection_scope) if reflection.scope
+ scope.merge!(preload_scope) if preload_scope
+ scope
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 38e231826c..0000000000
--- a/activerecord/lib/active_record/associations/preloader/belongs_to.rb
+++ /dev/null
@@ -1,15 +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 26690bf16d..0000000000
--- a/activerecord/lib/active_record/associations/preloader/collection_association.rb
+++ /dev/null
@@ -1,17 +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)
- 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 20df1cc19a..0000000000
--- a/activerecord/lib/active_record/associations/preloader/has_many.rb
+++ /dev/null
@@ -1,15 +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 5c5828262e..0000000000
--- a/activerecord/lib/active_record/associations/preloader/singular_association.rb
+++ /dev/null
@@ -1,18 +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
- 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 9d44a02021..a6b7ab80a2 100644
--- a/activerecord/lib/active_record/associations/preloader/through_association.rb
+++ b/activerecord/lib/active_record/associations/preloader/through_association.rb
@@ -1,106 +1,105 @@
+# frozen_string_literal: true
+
module ActiveRecord
module Associations
class Preloader
- module ThroughAssociation #:nodoc:
- def through_reflection
- reflection.through_reflection
- end
-
- def source_reflection
- reflection.source_reflection
- end
-
- def associated_records_by_owner(preloader)
- preloader.preload(owners,
- through_reflection.name,
- through_scope)
-
- through_records = owners.map do |owner|
- association = owner.association through_reflection.name
-
- center = target_records_from_association(association)
- [owner, Array(center)]
- end
-
- reset_association owners, through_reflection.name
-
- middle_records = through_records.flat_map { |(_, rec)| rec }
-
- preloaders = preloader.preload(middle_records,
- source_reflection.name,
- reflection_scope)
-
+ class ThroughAssociation < Association # :nodoc:
+ def run(preloader)
+ already_loaded = owners.first.association(through_reflection.name).loaded?
+ through_scope = through_scope()
+ reflection_scope = target_reflection_scope
+ through_preloaders = preloader.preload(owners, through_reflection.name, through_scope)
+ middle_records = through_preloaders.flat_map(&:preloaded_records)
+ preloaders = preloader.preload(middle_records, source_reflection.name, reflection_scope)
@preloaded_records = preloaders.flat_map(&:preloaded_records)
- middle_to_pl = preloaders.each_with_object({}) do |pl, h|
- pl.owners.each { |middle|
- h[middle] = pl
- }
- end
-
- through_records.each_with_object({}) do |(lhs, center), records_by_owner|
- pl_to_middle = center.group_by { |record| middle_to_pl[record] }
-
- records_by_owner[lhs] = pl_to_middle.flat_map do |pl, middles|
- rhs_records = middles.flat_map { |r|
- association = r.association source_reflection.name
-
- target_records_from_association(association)
- }.compact
-
- # 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
+ owners.each do |owner|
+ through_records = Array(owner.association(through_reflection.name).target)
+ 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
+ else
+ owner.association(through_reflection.name).reset if through_scope
+ end
+ result = through_records.flat_map do |record|
+ association = record.association(source_reflection.name)
+ target = association.target
+ association.reset if preload_scope
+ target
+ end
+ result.compact!
+ if reflection_scope
+ result.sort_by! { |rhs| preload_index[rhs] } if reflection_scope.order_values.any?
+ result.uniq! if reflection_scope.distinct_value
end
+ associate_records_to_owner(owner, result)
end
end
private
-
- def id_to_index_map(ids)
- id_map = {}
- ids.each_with_index { |id, index| id_map[id] = index }
- id_map
+ def through_reflection
+ reflection.through_reflection
end
- def reset_association(owners, association_name)
- should_reset = (through_scope != through_reflection.klass.unscoped) ||
- (reflection.options[:source_type] && through_reflection.collection?)
+ def source_reflection
+ reflection.source_reflection
+ end
- # Don't cache the association - we would only be caching a subset
- if should_reset
- owners.each { |owner|
- owner.association(association_name).reset
- }
+ def preload_index
+ @preload_index ||= @preloaded_records.each_with_object({}).with_index do |(id, result), index|
+ result[id] = index
end
end
def through_scope
scope = through_reflection.klass.unscoped
+ options = reflection.options
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
+ elsif !reflection_scope.where_clause.empty?
+ scope.where_clause = reflection_scope.where_clause
+ values = reflection_scope.values
+
+ if includes = values[:includes]
+ scope.includes!(source_reflection.name => includes)
+ else
+ scope.includes!(source_reflection.name)
end
- scope.references! reflection_scope.values[:references]
- if scope.eager_loading? && order_values = reflection_scope.values[:order]
+ 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
+
+ if left_outer_joins = values[:left_outer_joins]
+ scope.left_outer_joins!(source_reflection.name => left_outer_joins)
+ end
+
+ if scope.eager_loading? && order_values = values[:order]
scope = scope.order(order_values)
end
end
- scope
+ scope unless scope.empty_scope?
end
- def target_records_from_association(association)
- association.loaded? ? association.target : association.reader
+ def target_reflection_scope
+ if preload_scope
+ reflection_scope.merge(preload_scope)
+ elsif reflection.scope
+ reflection_scope
+ else
+ nil
+ end
end
end
end