/
tactical_eager_loading.rb
212 lines (199 loc) · 8.39 KB
/
tactical_eager_loading.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
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
201
202
203
204
205
206
207
208
209
210
211
212
# frozen-string-literal: true
module Sequel
module Plugins
# The tactical_eager_loading plugin allows you to eagerly load
# an association for all objects retrieved from the same dataset
# without calling +eager+ on the dataset. If you attempt to load
# associated objects for a record and the association for that
# object is currently not cached, it assumes you want to get
# the associated objects for all objects retrieved with the dataset that
# retrieved the current object.
#
# Tactical eager loading only takes affect if you retrieved the
# current object with Dataset#all, it doesn't work if you
# retrieved the current object with Dataset#each.
#
# Basically, this allows the following code to issue only two queries:
#
# Album.where{id<100}.all do |a|
# a.artists
# end
# # SELECT * FROM albums WHERE (id < 100)
# # SELECT * FROM artists WHERE id IN (...)
#
# Note that if you are passing a callback to the association method via
# a block or :callback option, or using the :reload option to reload
# the association, eager loading will not be done.
#
# You can use the :eager_reload option to reload the association for all
# objects that the current object was retrieved with:
#
# # SELECT * FROM albums WHERE (id < 100)
# albums = Album.where{id<100}.all
#
# # Eagerly load all artists for these albums
# # SELECT * FROM artists WHERE id IN (...)
# albums.first.artists
#
# # Do something that may affect which artists are associated to the albums
#
# # Eagerly reload all artists for these albums
# # SELECT * FROM artists WHERE id IN (...)
# albums.first.artists(eager_reload: true)
#
# You can also use the :eager option to specify dependent associations
# to eager load:
#
# albums = Album.where{id<100}.all
#
# # Eager load all artists for these albums, and all albums for those artists
# # SELECT * FROM artists WHERE id IN (...)
# # SELECT * FROM albums WHERE artist_id IN (...)
# albums.first.artists(eager: :albums)
#
# You can also use :eager to specify an eager callback. For example:
#
# albums = Album.where{id<100}.all
#
# # Eagerly load all artists whose name starts with A-M for these albums
# # SELECT * FROM artists WHERE name > 'N' AND id IN (...)
# albums.first.artists(eager: lambda{|ds| ds.where(Sequel[:name] > 'N')})
#
# Note that the :eager option only takes effect if the association
# has not already been loaded for the model.
#
# The tactical_eager_loading plugin also allows transparent eager
# loading when calling association methods on associated objects
# eagerly loaded via Dataset#eager_graph. This can reduce N queries
# to a single query when iterating over all associated objects.
# Consider the following code:
#
# artists = Artist.eager_graph(:albums).all
# artists.each do |artist|
# artist.albums.each do |album|
# album.tracks
# end
# end
#
# By default this will issue a single query to load the artists and
# albums, and then one query for each album to load the tracks for
# the album:
#
# # SELECT artists.id, ...
# albums.id, ...
# # FROM artists
# # LEFT OUTER JOIN albums ON (albums.artist_id = artists.id);
# # SELECT * FROM tracks WHERE album_id = 1;
# # SELECT * FROM tracks WHERE album_id = 2;
# # SELECT * FROM tracks WHERE album_id = 10;
# # ...
#
# With the tactical_eager_loading plugin, this uses the same
# query to load the artists and albums, but then issues a single query
# to load the tracks for all albums.
#
# # SELECT artists.id, ...
# albums.id, ...
# # FROM artists
# # LEFT OUTER JOIN albums ON (albums.artist_id = artists.id);
# # SELECT * FROM tracks WHERE (tracks.album_id IN (1, 2, 10, ...));
#
# Note that transparent eager loading for associated objects
# loaded by eager_graph will only take place if the associated classes
# also use the tactical_eager_loading plugin.
#
# When using this plugin, calling association methods on separate
# instances of the same result set is not thread-safe, because this
# plugin attempts to modify all instances of the same result set
# to eagerly set the associated objects, and having separate threads
# modify the same model instance is not thread-safe.
#
# Because this plugin will automatically use eager loading for
# performance, it can break code that defines associations that
# do not support eager loading, without marking that they do not
# support eager loading via the <tt>allow_eager: false</tt> option.
# Make sure to set <tt>allow_eager: false</tt> on any association
# used with this plugin if the association doesn't support eager loading.
#
# Usage:
#
# # Make all model subclass instances use tactical eager loading (called before loading subclasses)
# Sequel::Model.plugin :tactical_eager_loading
#
# # Make the Album class use tactical eager loading
# Album.plugin :tactical_eager_loading
module TacticalEagerLoading
module InstanceMethods
# The dataset that retrieved this object, set if the object was
# reteived via Dataset#all.
attr_accessor :retrieved_by
# All model objects retrieved with this object, set if the object was
# reteived via Dataset#all.
attr_accessor :retrieved_with
# Remove retrieved_by and retrieved_with when marshalling. retrieved_by
# contains unmarshallable objects, and retrieved_with can be very large
# and is not helpful without retrieved_by.
def marshallable!
@retrieved_by = nil
@retrieved_with = nil
super
end
private
# If there the association is not in the associations cache and the object
# was reteived via Dataset#all, eagerly load the association for all model
# objects retrieved with the current object.
def load_associated_objects(opts, dynamic_opts=OPTS, &block)
dynamic_opts = load_association_objects_options(dynamic_opts, &block)
name = opts[:name]
eager_reload = dynamic_opts[:eager_reload]
if (!associations.include?(name) || eager_reload) && opts[:allow_eager] != false && retrieved_by && !frozen? && !dynamic_opts[:callback] && !dynamic_opts[:reload]
retrieved_by.send(:eager_load, _filter_tactical_eager_load_objects(:eager_reload=>eager_reload, :name=>name), {name=>dynamic_opts[:eager] || OPTS}, model)
end
super
end
# Filter the objects used when tactical eager loading.
# By default, this removes frozen objects and objects that alreayd have the association loaded
def _filter_tactical_eager_load_objects(opts)
objects = defined?(super) ? super : retrieved_with.dup
if opts[:eager_reload]
objects.reject!(&:frozen?)
else
name = opts[:name]
objects.reject!{|x| x.frozen? || x.associations.include?(name)}
end
objects
end
end
module DatasetMethods
private
# Set the retrieved_with and retrieved_by attributes for each of the associated objects
# created by the eager graph loader with the appropriate class dataset and array of objects.
def _eager_graph_build_associations(_, egl)
objects = super
master = egl.master
egl.records_map.each do |k, v|
next if k == master || v.empty?
by = opts[:graph][:table_aliases][k]
values = v.values
values.each do |o|
next unless o.is_a?(TacticalEagerLoading::InstanceMethods) && !o.retrieved_by
o.retrieved_by = by
o.retrieved_with = values
end
end
objects
end
# Set the retrieved_with and retrieved_by attributes for each object
# with the current dataset and array of all objects.
def post_load(objects)
super
objects.each do |o|
next unless o.is_a?(Sequel::Model)
o.retrieved_by = self
o.retrieved_with = objects
end
end
end
end
end
end