aboutsummaryrefslogtreecommitdiffstats
path: root/activerecord/lib
diff options
context:
space:
mode:
Diffstat (limited to 'activerecord/lib')
-rw-r--r--activerecord/lib/active_record/associations/preloader.rb114
-rw-r--r--activerecord/lib/active_record/associations/preloader/association.rb53
-rw-r--r--activerecord/lib/active_record/associations/preloader/collection_association.rb4
-rw-r--r--activerecord/lib/active_record/associations/preloader/has_and_belongs_to_many.rb24
-rw-r--r--activerecord/lib/active_record/associations/preloader/has_many_through.rb2
-rw-r--r--activerecord/lib/active_record/associations/preloader/singular_association.rb4
-rw-r--r--activerecord/lib/active_record/associations/preloader/through_association.rb64
-rw-r--r--activerecord/lib/active_record/relation.rb3
8 files changed, 177 insertions, 91 deletions
diff --git a/activerecord/lib/active_record/associations/preloader.rb b/activerecord/lib/active_record/associations/preloader.rb
index 6cc9d6c079..127d0e2642 100644
--- a/activerecord/lib/active_record/associations/preloader.rb
+++ b/activerecord/lib/active_record/associations/preloader.rb
@@ -46,8 +46,6 @@ module ActiveRecord
autoload :BelongsTo, 'active_record/associations/preloader/belongs_to'
end
- attr_reader :records, :associations, :preload_scope, :model
-
# Eager loads the named associations for the given Active Record record(s).
#
# In this description, 'association name' shall refer to the name passed
@@ -82,40 +80,47 @@ module ActiveRecord
# [ :books, :author ]
# { author: :avatar }
# [ :books, { author: :avatar } ]
- def initialize(records, associations, preload_scope = nil)
- @records = Array.wrap(records).compact.uniq
- @associations = Array.wrap(associations)
- @preload_scope = preload_scope || NULL_RELATION
- end
NULL_RELATION = Struct.new(:values).new({})
- def run
- unless records.empty?
- associations.each { |association| preload(association) }
+ def preload(records, associations, preload_scope = nil)
+ records = Array.wrap(records).compact.uniq
+ associations = Array.wrap(associations)
+ preload_scope = preload_scope || NULL_RELATION
+
+ if records.empty?
+ []
+ else
+ associations.flat_map { |association|
+ preloaders_on association, records, preload_scope
+ }
end
end
private
- def preload(association)
+ def preloaders_on(association, records, scope)
case association
when Hash
- preload_hash(association)
+ preloaders_for_hash(association, records, scope)
when Symbol
- preload_one(association)
+ preloaders_for_one(association, records, scope)
when String
- preload_one(association.to_sym)
+ preloaders_for_one(association.to_sym, records, scope)
else
raise ArgumentError, "#{association.inspect} was not recognised for preload"
end
end
- def preload_hash(association)
- association.each do |parent, child|
- Preloader.new(records, parent, preload_scope).run
- Preloader.new(records.map { |record| record.send(parent) }.flatten, child).run
- end
+ def preloaders_for_hash(association, records, scope)
+ parent, child = association.to_a.first # hash should only be of length 1
+
+ loaders = preloaders_for_one parent, records, scope
+
+ recs = loaders.flat_map(&:preloaded_records).uniq
+ loaders.concat Array.wrap(child).flat_map { |assoc|
+ preloaders_on assoc, recs, scope
+ }
end
# Not all records have the same class, so group then preload group on the reflection
@@ -125,45 +130,76 @@ module ActiveRecord
# Additionally, polymorphic belongs_to associations can have multiple associated
# classes, depending on the polymorphic_type field. So we group by the classes as
# well.
- def preload_one(association)
- grouped_records(association).each do |reflection, klasses|
- klasses.each do |klass, records|
- preloader_for(reflection).new(klass, records, reflection, preload_scope).run
+ def preloaders_for_one(association, records, scope)
+ grouped_records(association, records).flat_map do |reflection, klasses|
+ klasses.map do |rhs_klass, rs|
+ loader = preloader_for(reflection, rs, rhs_klass).new(rhs_klass, rs, reflection, scope)
+ loader.run self
+ loader
end
end
end
- def grouped_records(association)
- Hash[
- records_by_reflection(association).map do |reflection, records|
- [reflection, records.group_by { |record| association_klass(reflection, record) }]
- end
- ]
+ def grouped_records(association, records)
+ reflection_records = records_by_reflection(association, records)
+
+ reflection_records.each_with_object({}) do |(reflection, r_records),h|
+ h[reflection] = r_records.group_by { |record|
+ association_klass(reflection, record)
+ }
+ end
end
- def records_by_reflection(association)
+ def records_by_reflection(association, records)
records.group_by do |record|
- reflection = record.class.reflections[association]
+ reflection = record.class.reflect_on_association(association)
- unless reflection
- raise ActiveRecord::ConfigurationError, "Association named '#{association}' was not found; " \
- "perhaps you misspelled it?"
- end
-
- reflection
+ reflection || raise_config_error(association)
end
end
+ def raise_config_error(association)
+ raise ActiveRecord::ConfigurationError,
+ "Association named '#{association}' was not found; " \
+ "perhaps you misspelled it?"
+ end
+
def association_klass(reflection, record)
if reflection.macro == :belongs_to && reflection.options[:polymorphic]
- klass = record.send(reflection.foreign_type)
+ klass = record.read_attribute(reflection.foreign_type.to_s)
klass && klass.constantize
else
reflection.klass
end
end
- def preloader_for(reflection)
+ class AlreadyLoaded
+ attr_reader :owners, :reflection
+
+ def initialize(klass, owners, reflection, preload_scope)
+ @owners = owners
+ @reflection = reflection
+ end
+
+ def run(preloader); end
+
+ def preloaded_records
+ owners.flat_map { |owner| owner.read_attribute reflection.name }
+ end
+ end
+
+ class NullPreloader
+ def self.new(klass, owners, reflection, preload_scope); self; end
+ def self.run(preloader); end
+ end
+
+ def preloader_for(reflection, owners, rhs_klass)
+ return NullPreloader unless rhs_klass
+
+ if owners.first.association(reflection.name).loaded?
+ return AlreadyLoaded
+ end
+
case reflection.macro
when :has_many
reflection.options[:through] ? HasManyThrough : HasMany
diff --git a/activerecord/lib/active_record/associations/preloader/association.rb b/activerecord/lib/active_record/associations/preloader/association.rb
index 928da71eed..69b65982b3 100644
--- a/activerecord/lib/active_record/associations/preloader/association.rb
+++ b/activerecord/lib/active_record/associations/preloader/association.rb
@@ -3,6 +3,7 @@ module ActiveRecord
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
@@ -12,15 +13,14 @@ module ActiveRecord
@model = owners.first && owners.first.class
@scope = nil
@owners_by_key = nil
+ @preloaded_records = []
end
- def run
- unless owners.first.association(reflection.name).loaded?
- preload
- end
+ def run(preloader)
+ preload(preloader)
end
- def preload
+ def preload(preloader)
raise NotImplementedError
end
@@ -68,36 +68,39 @@ module ActiveRecord
private
- def associated_records_by_owner
+ def associated_records_by_owner(preloader)
owners_map = owners_by_key
owner_keys = owners_map.keys.compact
# Each record may have multiple owners, and vice-versa
- records_by_owner = Hash[owners.map { |owner| [owner, []] }]
+ records_by_owner = owners.each_with_object({}) do |owner,h|
+ h[owner] = []
+ end
- if klass && owner_keys.any?
+ if owner_keys.any?
# 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
sliced = owner_keys.each_slice(klass.connection.in_clause_length || owner_keys.size)
- sliced.each { |slice|
- records = records_for(slice)
- caster = type_caster(records, association_key_name)
- records.each do |record|
- owner_key = caster.call record[association_key_name]
-
- owners_map[owner_key].each do |owner|
- records_by_owner[owner] << record
- end
+
+ records = load_slices sliced
+ records.each do |record, owner_key|
+ owners_map[owner_key].each do |owner|
+ records_by_owner[owner] << record
end
- }
+ end
end
records_by_owner
end
- IDENTITY = lambda { |value| value }
- def type_caster(results, name)
- IDENTITY
+ def load_slices(slices)
+ @preloaded_records = slices.flat_map { |slice|
+ records_for(slice)
+ }
+
+ @preloaded_records.map { |record|
+ [record, record[association_key_name]]
+ }
end
def reflection_scope
@@ -116,6 +119,14 @@ module ActiveRecord
scope.select! preload_values[:select] || values[:select] || table[Arel.star]
scope.includes! preload_values[:includes] || values[:includes]
+ if preload_values.key? :order
+ scope.order! preload_values[:order]
+ else
+ if values.key? :order
+ scope.order! values[:order]
+ end
+ end
+
if options[:as]
scope.where!(klass.table_name => { reflection.type => model.base_class.sti_name })
end
diff --git a/activerecord/lib/active_record/associations/preloader/collection_association.rb b/activerecord/lib/active_record/associations/preloader/collection_association.rb
index e6cd35e7a1..5adffcd831 100644
--- a/activerecord/lib/active_record/associations/preloader/collection_association.rb
+++ b/activerecord/lib/active_record/associations/preloader/collection_association.rb
@@ -9,8 +9,8 @@ module ActiveRecord
super.order(preload_scope.values[:order] || reflection_scope.values[:order])
end
- def preload
- associated_records_by_owner.each do |owner, records|
+ def preload(preloader)
+ associated_records_by_owner(preloader).each do |owner, records|
association = owner.association(reflection.name)
association.loaded!
association.target.concat(records)
diff --git a/activerecord/lib/active_record/associations/preloader/has_and_belongs_to_many.rb b/activerecord/lib/active_record/associations/preloader/has_and_belongs_to_many.rb
index c042a44b21..b62ca6f681 100644
--- a/activerecord/lib/active_record/associations/preloader/has_and_belongs_to_many.rb
+++ b/activerecord/lib/active_record/associations/preloader/has_and_belongs_to_many.rb
@@ -33,16 +33,22 @@ module ActiveRecord
# Once we have used the join table column (in super), we manually instantiate the
# actual records, ensuring that we don't create more than one instances of the same
# record
- def associated_records_by_owner
- records = {}
- super.each_value do |rows|
- rows.map! { |row| records[row[klass.primary_key]] ||= klass.instantiate(row) }
- end
- end
+ def load_slices(slices)
+ identity_map = {}
+ caster = nil
+ name = association_key_name
+
+ records_to_keys = slices.flat_map { |slice|
+ records = records_for(slice)
+ caster ||= records.column_types.fetch(name, records.identity_type)
+ records.map! { |row|
+ record = identity_map[row[klass.primary_key]] ||= klass.instantiate(row)
+ [record, caster.type_cast(row[name])]
+ }
+ }
+ @preloaded_records = records_to_keys.map(&:first)
- def type_caster(results, name)
- caster = results.column_types.fetch(name, results.identity_type)
- lambda { |value| caster.type_cast value }
+ records_to_keys
end
def build_scope
diff --git a/activerecord/lib/active_record/associations/preloader/has_many_through.rb b/activerecord/lib/active_record/associations/preloader/has_many_through.rb
index 157b627ad5..7b37b5942d 100644
--- a/activerecord/lib/active_record/associations/preloader/has_many_through.rb
+++ b/activerecord/lib/active_record/associations/preloader/has_many_through.rb
@@ -4,7 +4,7 @@ module ActiveRecord
class HasManyThrough < CollectionAssociation #:nodoc:
include ThroughAssociation
- def associated_records_by_owner
+ def associated_records_by_owner(preloader)
records_by_owner = super
if reflection_scope.distinct_value
diff --git a/activerecord/lib/active_record/associations/preloader/singular_association.rb b/activerecord/lib/active_record/associations/preloader/singular_association.rb
index 44e804d785..2b5cfda8ce 100644
--- a/activerecord/lib/active_record/associations/preloader/singular_association.rb
+++ b/activerecord/lib/active_record/associations/preloader/singular_association.rb
@@ -5,8 +5,8 @@ module ActiveRecord
private
- def preload
- associated_records_by_owner.each do |owner, associated_records|
+ def preload(preloader)
+ associated_records_by_owner(preloader).each do |owner, associated_records|
record = associated_records.first
association = owner.association(reflection.name)
diff --git a/activerecord/lib/active_record/associations/preloader/through_association.rb b/activerecord/lib/active_record/associations/preloader/through_association.rb
index 2c625cec04..ea21836c65 100644
--- a/activerecord/lib/active_record/associations/preloader/through_association.rb
+++ b/activerecord/lib/active_record/associations/preloader/through_association.rb
@@ -2,7 +2,6 @@ module ActiveRecord
module Associations
class Preloader
module ThroughAssociation #:nodoc:
-
def through_reflection
reflection.through_reflection
end
@@ -11,34 +10,67 @@ module ActiveRecord
reflection.source_reflection
end
- def associated_records_by_owner
- through_records = through_records_by_owner
+ def associated_records_by_owner(preloader)
+ preloader.preload(owners,
+ through_reflection.name,
+ through_scope)
+
+ through_records = owners.map do |owner, h|
+ association = owner.association through_reflection.name
+
+ [owner, Array(association.reader)]
+ end
+
+ reset_association owners, through_reflection.name
- Preloader.new(through_records.values.flatten, source_reflection.name, reflection_scope).run
+ middle_records = through_records.map { |(_,rec)| rec }.flatten
- through_records.each do |owner, records|
- records.map! { |r| r.send(source_reflection.name) }.flatten!
- records.compact!
+ preloaders = preloader.preload(middle_records,
+ source_reflection.name,
+ reflection_scope)
+
+ middle_to_pl = preloaders.each_with_object({}) do |pl,h|
+ pl.owners.each { |middle|
+ h[middle] = pl
+ }
end
+
+ through_records.each_with_object({}) { |(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|
+ r.send(source_reflection.name)
+ }.compact
+
+ loaded_records = pl.preloaded_records
+ i = 0
+ record_index = loaded_records.each_with_object({}) { |r,indexes|
+ indexes[r] = i
+ i += 1
+ }
+ records = rhs_records.sort_by { |rhs| record_index[rhs] }
+ @preloaded_records.concat rhs_records
+ records
+ end
+ }
end
private
- def through_records_by_owner
- Preloader.new(owners, through_reflection.name, through_scope).run
-
+ def reset_association(owners, association_name)
should_reset = (through_scope != through_reflection.klass.unscoped) ||
(reflection.options[:source_type] && through_reflection.collection?)
- owners.each_with_object({}) do |owner, h|
- association = owner.association through_reflection.name
- h[owner] = Array(association.reader)
-
- # Dont cache the association - we would only be caching a subset
- association.reset if should_reset
+ # Dont cache the association - we would only be caching a subset
+ if should_reset
+ owners.each { |owner|
+ owner.association(association_name).reset
+ }
end
end
+
def through_scope
scope = through_reflection.klass.unscoped
diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb
index 4e86e905ed..cfaf566ec4 100644
--- a/activerecord/lib/active_record/relation.rb
+++ b/activerecord/lib/active_record/relation.rb
@@ -599,8 +599,9 @@ module ActiveRecord
preload = preload_values
preload += includes_values unless eager_loading?
+ preloader = ActiveRecord::Associations::Preloader.new
preload.each do |associations|
- ActiveRecord::Associations::Preloader.new(@records, associations).run
+ preloader.preload @records, associations
end
@records.each { |record| record.readonly! } if readonly_value