1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200
|
# frozen_string_literal: true
module ActiveRecord
module Associations
# Implements the details of eager loading of Active Record associations.
#
# Suppose that you have the following two Active Record models:
#
# class Author < ActiveRecord::Base
# # columns: name, age
# has_many :books
# end
#
# class Book < ActiveRecord::Base
# # columns: title, sales, author_id
# end
#
# 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.
#
class Preloader #:nodoc:
extend ActiveSupport::Autoload
eager_autoload do
autoload :Association, "active_record/associations/preloader/association"
autoload :ThroughAssociation, "active_record/associations/preloader/through_association"
end
# Eager loads the named associations for the given Active Record record(s).
#
# In this description, 'association name' shall refer to the name passed
# to an association creation method. For example, a model that specifies
# <tt>belongs_to :author</tt>, <tt>has_many :buyers</tt> has association
# names +:author+ and +:buyers+.
#
# == Parameters
# +records+ is an array of ActiveRecord::Base. This array needs not be flat,
# i.e. +records+ itself may also contain arrays of records. In any case,
# +preload_associations+ will preload the all associations records by
# flattening +records+.
#
# +associations+ specifies one or more associations that you want to
# preload. It may be:
# - a Symbol or a String which specifies a single association name. For
# example, specifying +:books+ allows this method to preload all books
# for an Author.
# - an Array which specifies multiple association names. This array
# is processed recursively. For example, specifying <tt>[:avatar, :books]</tt>
# allows this method to preload an author's avatar as well as all of his
# books.
# - a Hash which specifies multiple association names, as well as
# association names for the to-be-preloaded association objects. For
# example, specifying <tt>{ author: :avatar }</tt> will preload a
# book's author, as well as that author's avatar.
#
# +:associations+ has the same format as the +:include+ option for
# <tt>ActiveRecord::Base.find</tt>. So +associations+ could look like this:
#
# :books
# [ :books, :author ]
# { author: :avatar }
# [ :books, { author: :avatar } ]
def preload(records, associations, preload_scope = nil)
records = Array.wrap(records).compact
if records.empty?
[]
else
Array.wrap(associations).flat_map { |association|
preloaders_on association, records, preload_scope
}
end
end
private
# Loads all the given data into +records+ for the +association+.
def preloaders_on(association, records, scope, polymorphic_parent = false)
case association
when Hash
preloaders_for_hash(association, records, scope, polymorphic_parent)
when Symbol, String
preloaders_for_one(association, records, scope, polymorphic_parent)
else
raise ArgumentError, "#{association.inspect} was not recognized for preload"
end
end
def preloaders_for_hash(association, records, scope, polymorphic_parent)
association.flat_map { |parent, child|
grouped_records(parent, records, polymorphic_parent).flat_map do |reflection, reflection_records|
loaders = preloaders_for_reflection(reflection, reflection_records, scope)
recs = loaders.flat_map(&:preloaded_records).uniq
child_polymorphic_parent = reflection && reflection.options[:polymorphic]
loaders.concat Array.wrap(child).flat_map { |assoc|
preloaders_on assoc, recs, scope, child_polymorphic_parent
}
loaders
end
}
end
# Loads all the given data into +records+ for a singular +association+.
#
# Functions by instantiating a preloader class such as Preloader::Association and
# call the +run+ method for each passed in class in the +records+ argument.
#
# Not all records have the same class, so group then preload group on the reflection
# itself so that if various subclass share the same association then we do not split
# them unnecessarily
#
# 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 preloaders_for_one(association, records, scope, polymorphic_parent)
grouped_records(association, records, polymorphic_parent)
.flat_map do |reflection, reflection_records|
preloaders_for_reflection reflection, reflection_records, scope
end
end
def preloaders_for_reflection(reflection, records, scope)
records.group_by { |record| record.association(reflection.name).klass }.map do |rhs_klass, rs|
preloader_for(reflection, rs).new(rhs_klass, rs, reflection, scope).run
end
end
def grouped_records(association, records, polymorphic_parent)
h = {}
records.each do |record|
reflection = record.class._reflect_on_association(association)
next if polymorphic_parent && !reflection || !record.association(association).klass
(h[reflection] ||= []) << record
end
h
end
class AlreadyLoaded # :nodoc:
def initialize(klass, owners, reflection, preload_scope)
@owners = owners
@reflection = reflection
end
def run
self
end
def preloaded_records
@preloaded_records ||= records_by_owner.flat_map(&:last)
end
def records_by_owner
@records_by_owner ||= owners.each_with_object({}) do |owner, result|
result[owner] = Array(owner.association(reflection.name).target)
end
end
private
attr_reader :owners, :reflection
end
# Returns a class containing the logic needed to load preload the data
# and attach it to a relation. The class returned implements a `run` method
# that accepts a preloader.
def preloader_for(reflection, owners)
if owners.all? { |o| o.association(reflection.name).loaded? }
return AlreadyLoaded
end
reflection.check_preloadable!
if reflection.options[:through]
ThroughAssociation
else
Association
end
end
end
end
end
|