Permalink
Browse files

Allow specifying a callback to customize eager loading at query time.

This is analogous to the way the :eager_block association option
allows you to customize it at association definition time. For example,
if you wanted artists with their albums since 1990:

  Artist.eager(:albums => proc {|ds| ds.filter(:year > 1990)})

To cascade eager loading using this method, call eager/eager_graph inside the
callback:

  Artist.eager(:albums => proc {|ds| ds.filter(:year > 1990).eager(:tracks => :genre)})
  • Loading branch information...
1 parent 4dc394a commit acd7b6d6a79b8df63201dbfa6427c688d8b75ffb John Firebaugh committed with Mar 1, 2011
Showing with 103 additions and 2 deletions.
  1. +2 −0 CHANGELOG
  2. +21 −2 lib/sequel/model/associations.rb
  3. +80 −0 spec/model/eager_loading_spec.rb
View
@@ -1,5 +1,7 @@
=== HEAD
+* Allow specifying a callback to customize eager loading at query time (jfirebaugh)
+
* Fix bug in the identity_map plugin for many_to_one associations when the association reflection hadn't been filled in yet (funny-falcon)
* Add serialization_modification_detection plugin for detecting changes in serialized columns (jeremyevans) (#333)
@@ -688,6 +688,7 @@ def eager_loading_dataset(opts, ds, select, associations, eager_options={})
end
ds = ds.eager(associations) unless Array(associations).empty?
ds = opts[:eager_block].call(ds) if opts[:eager_block]
+ ds = eager_options[:eager_block].call(ds) if eager_options[:eager_block]
ds
end
@@ -1258,6 +1259,19 @@ def set_one_to_one_associated_object(opts, o)
# Artist.eager_graph(:albums=>:tracks).all
# Artist.eager(:albums=>{:tracks=>:genre}).all
# Artist.eager_graph(:albums=>{:tracks=>:genre}).all
+ #
+ # You can also pass a callback as a hash value in order to customize the dataset being
+ # eager loaded at query time, analogous to the way the :eager_block association option
+ # allows you to customize it at association definition time. For example,
+ # if you wanted artists with their albums since 1990:
+ #
+ # Artist.eager(:albums => proc {|ds| ds.filter(:year > 1990)})
+ #
+ # To cascade eager loading using this method, call +eager+/+eager_graph+ inside the
+ # callback:
+ #
+ # Artist.eager(:albums => proc {|ds| ds.filter(:year > 1990).eager(:tracks => :genre)})
+ #
module DatasetMethods
# Add the <tt>eager!</tt> and <tt>eager_graph!</tt> mutation methods to the dataset.
def self.extended(obj)
@@ -1555,10 +1569,15 @@ def eager_load(a, eager_assoc=@opts[:eager])
reflections.each do |r|
loader = r[:eager_loader]
+ associations = eager_assoc[r[:name]]
+ if associations.respond_to?(:call)
+ eager_block = associations
+ associations = {}
+ end
if loader.arity == 1
- loader.call(:key_hash=>key_hash, :rows=>a, :associations=>eager_assoc[r[:name]], :self=>self)
+ loader.call(:key_hash=>key_hash, :rows=>a, :associations=>associations, :self=>self, :eager_block=>eager_block)
else
- loader.call(key_hash, a, eager_assoc[r[:name]])
+ loader.call(key_hash, a, associations)
end
a.each{|object| object.send(:run_association_callbacks, r, :after_load, object.associations[r[:name]])} unless r[:after_load].empty?
end
@@ -646,6 +646,86 @@ def fetch_rows(sql)
a.should == EagerAlbum.load(:id => 1, :band_id => 2)
a.al_genres.should == [EagerGenre.load(:id=>4)]
end
+
+ it "should eagerly load a many_to_one association with custom eager block" do
+ a = EagerAlbum.eager(:band => proc {|ds| ds.select(:id, :name)}).all
+ a.should be_a_kind_of(Array)
+ a.size.should == 1
+ a.first.should be_a_kind_of(EagerAlbum)
+ a.first.values.should == {:id => 1, :band_id => 2}
+ MODEL_DB.sqls.should == ['SELECT * FROM albums', 'SELECT id, name FROM bands WHERE (bands.id IN (2))']
+ a = a.first
+ a.band.should be_a_kind_of(EagerBand)
+ a.band.values.should == {:id => 2}
+ MODEL_DB.sqls.length.should == 2
+ end
+
+ it "should eagerly load a one_to_one association with custom eager block" do
+ EagerAlbum.one_to_one :track, :class=>'EagerTrack', :key=>:album_id
+ a = EagerAlbum.eager(:track => proc {|ds| ds.select(:id)}).all
+ a.should == [EagerAlbum.load(:id => 1, :band_id => 2)]
+ MODEL_DB.sqls.should == ['SELECT * FROM albums', 'SELECT id FROM tracks WHERE (tracks.album_id IN (1))']
+ a.first.track.should == EagerTrack.load(:id => 3, :album_id=>1)
+ MODEL_DB.sqls.length.should == 2
+ end
+
+ it "should eagerly load a one_to_many association with custom eager block" do
+ a = EagerAlbum.eager(:tracks => proc {|ds| ds.select(:id)}).all
+ a.should be_a_kind_of(Array)
+ a.size.should == 1
+ a.first.should be_a_kind_of(EagerAlbum)
+ a.first.values.should == {:id => 1, :band_id => 2}
+ MODEL_DB.sqls.should == ['SELECT * FROM albums', 'SELECT id FROM tracks WHERE (tracks.album_id IN (1))']
+ a = a.first
+ a.tracks.should be_a_kind_of(Array)
+ a.tracks.size.should == 1
+ a.tracks.first.should be_a_kind_of(EagerTrack)
+ a.tracks.first.values.should == {:id => 3, :album_id=>1}
+ MODEL_DB.sqls.length.should == 2
+ end
+
+ it "should eagerly load a many_to_many association with custom eager block" do
+ a = EagerAlbum.eager(:genres => proc {|ds| ds.select(:name, ds.opts[:select].last)}).all
+ a.should be_a_kind_of(Array)
+ a.size.should == 1
+ a.first.should be_a_kind_of(EagerAlbum)
+ a.first.values.should == {:id => 1, :band_id => 2}
+ MODEL_DB.sqls.should == ['SELECT * FROM albums', "SELECT name, ag.album_id AS x_foreign_key_x FROM genres INNER JOIN ag ON ((ag.genre_id = genres.id) AND (ag.album_id IN (1)))"]
+ a = a.first
+ a.genres.should be_a_kind_of(Array)
+ a.genres.size.should == 1
+ a.genres.first.should be_a_kind_of(EagerGenre)
+ a.genres.first.values.should == {:id => 4}
+ MODEL_DB.sqls.length.should == 2
+ end
+
+ it "should allow cascading of eager loading within a custom eager block" do
+ a = EagerTrack.eager(:album => proc {|ds| ds.eager(:band => :members)}).all
+ a.should be_a_kind_of(Array)
+ a.size.should == 1
+ a.first.should be_a_kind_of(EagerTrack)
+ a.first.values.should == {:id => 3, :album_id => 1}
+ MODEL_DB.sqls.length.should == 4
+ MODEL_DB.sqls.should == ['SELECT * FROM tracks',
+ 'SELECT * FROM albums WHERE (albums.id IN (1))',
+ 'SELECT * FROM bands WHERE (bands.id IN (2))',
+ "SELECT members.*, bm.band_id AS x_foreign_key_x FROM members INNER JOIN bm ON ((bm.member_id = members.id) AND (bm.band_id IN (2)))"]
+ a = a.first
+ a.album.should be_a_kind_of(EagerAlbum)
+ a.album.values.should == {:id => 1, :band_id => 2}
+ a.album.band.should be_a_kind_of(EagerBand)
+ a.album.band.values.should == {:id => 2}
+ a.album.band.members.should be_a_kind_of(Array)
+ a.album.band.members.size.should == 1
+ a.album.band.members.first.should be_a_kind_of(EagerBandMember)
+ a.album.band.members.first.values.should == {:id => 5}
+ MODEL_DB.sqls.length.should == 4
+ end
+
+ it "should call both association and custom eager blocks" do
+ EagerBand.eager(:good_albums => proc {|ds| ds.select(:name)}).all
+ MODEL_DB.sqls.should == ['SELECT * FROM bands', "SELECT name FROM albums WHERE ((albums.band_id IN (2)) AND (name = 'good'))"]
+ end
end
describe Sequel::Model, "#eager_graph" do

0 comments on commit acd7b6d

Please sign in to comment.