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
|