-
Notifications
You must be signed in to change notification settings - Fork 21.4k
/
preloader.rb
132 lines (121 loc) · 5.25 KB
/
preloader.rb
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
# frozen_string_literal: true
require "active_support/core_ext/enumerable"
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 :Batch, "active_record/associations/preloader/batch"
autoload :Branch, "active_record/associations/preloader/branch"
autoload :ThroughAssociation, "active_record/associations/preloader/through_association"
end
attr_reader :records, :associations, :scope, :associate_by_default
# 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 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+ method in
# <tt>ActiveRecord::QueryMethods</tt>. So +associations+ could look like this:
#
# :books
# [ :books, :author ]
# { author: :avatar }
# [ :books, { author: :avatar } ]
#
# +available_records+ is an array of ActiveRecord::Base. The Preloader
# will try to use the objects in this array to preload the requested
# associations before querying the database. This can save database
# queries by reusing in-memory objects. The optimization is only applied
# to single associations (i.e. :belongs_to, :has_one) with no scopes.
def initialize(records:, associations:, scope: nil, available_records: [], associate_by_default: true)
@records = records
@associations = associations
@scope = scope
@available_records = available_records || []
@associate_by_default = associate_by_default
@tree = Branch.new(
parent: nil,
association: nil,
children: @associations,
associate_by_default: @associate_by_default,
scope: @scope
)
@tree.preloaded_records = @records
end
def empty?
associations.nil? || records.length == 0
end
def call
Batch.new([self], available_records: @available_records).call
loaders
end
def branches
@tree.children
end
def loaders
branches.flat_map(&:loaders)
end
end
end
end