aboutsummaryrefslogtreecommitdiffstats
path: root/activerecord/lib/active_record/associations/preloader.rb
diff options
context:
space:
mode:
Diffstat (limited to 'activerecord/lib/active_record/associations/preloader.rb')
-rw-r--r--activerecord/lib/active_record/associations/preloader.rb169
1 files changed, 98 insertions, 71 deletions
diff --git a/activerecord/lib/active_record/associations/preloader.rb b/activerecord/lib/active_record/associations/preloader.rb
index 0848e7afb3..4358f3b581 100644
--- a/activerecord/lib/active_record/associations/preloader.rb
+++ b/activerecord/lib/active_record/associations/preloader.rb
@@ -2,33 +2,42 @@ module ActiveRecord
module Associations
# Implements the details of eager loading of Active Record associations.
#
- # Note that 'eager loading' and 'preloading' are actually the same thing.
- # However, there are two different eager loading strategies.
+ # Suppose that you have the following two Active Record models:
#
- # The first one is by using table joins. This was only strategy available
- # prior to Rails 2.1. Suppose that you have an Author model with columns
- # 'name' and 'age', and a Book model with columns 'name' and 'sales'. Using
- # this strategy, Active Record would try to retrieve all data for an author
- # and all of its books via a single query:
+ # class Author < ActiveRecord::Base
+ # # columns: name, age
+ # has_many :books
+ # end
#
- # SELECT * FROM authors
- # LEFT OUTER JOIN books ON authors.id = books.author_id
- # WHERE authors.name = 'Ken Akamatsu'
+ # class Book < ActiveRecord::Base
+ # # columns: title, sales
+ # end
#
- # However, this could result in many rows that contain redundant data. After
- # having received the first row, we already have enough data to instantiate
- # the Author object. In all subsequent rows, only the data for the joined
- # 'books' table is useful; the joined 'authors' data is just redundant, and
- # processing this redundant data takes memory and CPU time. The problem
- # quickly becomes worse and worse as the level of eager loading increases
- # (i.e. if Active Record is to eager load the associations' associations as
- # well).
+ # When you load an author with all associated books Active Record will make
+ # multiple queries like this:
+ #
+ # Author.includes(:books).where(:name => ['bell hooks', 'Homer').to_a
+ #
+ # => SELECT `authors`.* FROM `authors` WHERE `name` IN ('bell hooks', 'Homer')
+ # => SELECT `books`.* FROM `books` WHERE `author_id` IN (2, 5)
+ #
+ # Active Record saves the ids of the records from the first query to use in
+ # the second. Depending on the number of associations involved there can be
+ # arbitrarily many SQL queries made.
+ #
+ # However, if there is a WHERE clause that spans across tables Active
+ # Record will fall back to a slightly more resource-intensive single query:
+ #
+ # Author.includes(:books).where(books: {title: 'Illiad'}).to_a
+ # => SELECT `authors`.`id` AS t0_r0, `authors`.`name` AS t0_r1, `authors`.`age` AS t0_r2,
+ # `books`.`id` AS t1_r0, `books`.`title` AS t1_r1, `books`.`sales` AS t1_r2
+ # FROM `authors`
+ # LEFT OUTER JOIN `books` ON `authors`.`id` = `books`.`author_id`
+ # WHERE `books`.`title` = 'Illiad'
+ #
+ # This could result in many rows that contain redundant data and it performs poorly at scale
+ # and is therefore only used when necessary.
#
- # The second strategy is to use multiple database queries, one for each
- # level of association. Since Rails 2.1, this is the default strategy. In
- # situations where a table join is necessary (e.g. when the +:conditions+
- # option references an association's column), it will fallback to the table
- # join strategy.
class Preloader #:nodoc:
extend ActiveSupport::Autoload
@@ -42,12 +51,9 @@ module ActiveRecord
autoload :HasManyThrough, 'active_record/associations/preloader/has_many_through'
autoload :HasOne, 'active_record/associations/preloader/has_one'
autoload :HasOneThrough, 'active_record/associations/preloader/has_one_through'
- autoload :HasAndBelongsToMany, 'active_record/associations/preloader/has_and_belongs_to_many'
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,36 +88,48 @@ 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 || Relation.new(nil, nil)
- end
- def run
- unless records.empty?
- associations.each { |association| preload(association) }
+ NULL_RELATION = Struct.new(:values, :bind_values).new({}, [])
+
+ 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)
- when String, Symbol
- preload_one(association.to_sym)
+ preloaders_for_hash(association, records, scope)
+ when Symbol
+ preloaders_for_one(association, records, scope)
+ when String
+ 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)
+ association.flat_map { |parent, child|
+ 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
+ }
+ loaders
+ }
end
# Not all records have the same class, so group then preload group on the reflection
@@ -121,52 +139,61 @@ 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)
+ h = {}
+ records.each do |record|
+ next unless record
+ assoc = record.association(association)
+ klasses = h[assoc.reflection] ||= {}
+ (klasses[assoc.klass] ||= []) << record
+ end
+ h
end
- def records_by_reflection(association)
- records.group_by do |record|
- reflection = record.class.reflections[association]
+ class AlreadyLoaded
+ attr_reader :owners, :reflection
- unless reflection
- raise ActiveRecord::ConfigurationError, "Association named '#{association}' was not found; " \
- "perhaps you misspelled it?"
- end
+ def initialize(klass, owners, reflection, preload_scope)
+ @owners = owners
+ @reflection = reflection
+ end
+
+ def run(preloader); end
- reflection
+ def preloaded_records
+ owners.flat_map { |owner| owner.association(reflection.name).target }
end
end
- def association_klass(reflection, record)
- if reflection.macro == :belongs_to && reflection.options[:polymorphic]
- klass = record.send(reflection.foreign_type)
- klass && klass.constantize
- else
- reflection.klass
- end
+ class NullPreloader
+ def self.new(klass, owners, reflection, preload_scope); self; end
+ def self.run(preloader); end
+ def self.preloaded_records; []; end
end
- def preloader_for(reflection)
+ def preloader_for(reflection, owners, rhs_klass)
+ return NullPreloader unless rhs_klass
+
+ if owners.first.association(reflection.name).loaded?
+ return AlreadyLoaded
+ end
+ reflection.check_preloadable!
+
case reflection.macro
when :has_many
reflection.options[:through] ? HasManyThrough : HasMany
when :has_one
reflection.options[:through] ? HasOneThrough : HasOne
- when :has_and_belongs_to_many
- HasAndBelongsToMany
when :belongs_to
BelongsTo
end