module Sequel::Plugins::TacticalEagerLoading

  1. lib/sequel/plugins/tactical_eager_loading.rb

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 allow_eager: false option. Make sure to set allow_eager: false 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