Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Support dynamic callbacks to customize regular association loading at…

… query time

This brings the dynamic callback feature recently added to eager and
eager_graph and applies it to regular association loading.  You can
now apply a dynamic callback for regular associations by passing
a block to the association method:

  artist.albums{|ds| ds.where{year > 1990}}

On ruby 1.8.6, that isn't supported, as 1.8.6 doesn't allow blocks
to take block arguments.  So there you have a pass the proc as
an argument:

  artist.albums(proc{|ds| ds.where{year > 1990}})

This breaks backwards compatibility in the private API for the
load_associated_objects and _load_associated_objects methods.
They now take 2 arguments instead of 1, the new argument being
a dynamic options hash.

While here, fix some breakage on ruby 1.9 for multiple
assignment inside a conditional.
  • Loading branch information...
commit 3b633ee373c6e2e922ed68138f3ac27bc6bf70ae 1 parent dbb35b6
@jeremyevans authored
View
6 CHANGELOG
@@ -1,12 +1,14 @@
=== HEAD
-* Support cascading of eager loading using an intermediate proc for both eager and eager_graph (jeremyevans)
+* Support dynamic callbacks to customize regular association loading at query time (jeremyevans)
+
+* Support cascading of eager loading with dynamic callbacks for both eager and eager_graph (jeremyevans)
* Make the xml_serializer plugin handle namespaced models by using __ instead of / as a separator (jeremyevans)
* Allow the :eager_grapher association proc to accept a single hash instead of 3 arguments (jfirebaugh)
-* Allow specifying a proc callback to customize eager loading at query time (jfirebaugh, jeremyevans)
+* Support dynamic callbacks to customize eager loading at query time (jfirebaugh, jeremyevans)
* Fix bug in the identity_map plugin for many_to_one associations when the association reflection hadn't been filled in yet (funny-falcon)
View
58 lib/sequel/model/associations.rb
@@ -759,9 +759,20 @@ def def_association_dataset_methods(opts)
def_association_method(opts)
end
- # Adds the association method to the association methods module.
- def def_association_method(opts)
- association_module_def(opts.association_method, opts){|*reload| load_associated_objects(opts, reload[0])}
+ # Adds the association method to the association methods module. Be backwards
+ # compatible with ruby 1.8.6, which doesn't support blocks taking block arguments.
+ if RUBY_VERSION >= '1.8.7'
+ class_eval <<-END, __FILE__, __LINE__+1
+ def def_association_method(opts)
+ association_module_def(opts.association_method, opts){|*dynamic_opts, &block| load_associated_objects(opts, dynamic_opts[0], &block)}
+ end
+ END
+ else
+ class_eval <<-END, __FILE__, __LINE__+1
+ def def_association_method(opts)
+ association_module_def(opts.association_method, opts){|*dynamic_opts| load_associated_objects(opts, dynamic_opts[0])}
+ end
+ END
end
# Configures many_to_many association reflection and adds the related association methods
@@ -1050,6 +1061,15 @@ def _apply_association_options(opts, ds)
ds
end
+ # Return a dataset for the association after applying any dynamic callback.
+ def _associated_dataset(opts, dynamic_opts)
+ ds = send(opts.dataset_method)
+ if callback = dynamic_opts[:callback]
+ ds = callback.call(ds)
+ end
+ ds
+ end
+
# Return an association dataset for the given association reflection
def _dataset(opts)
raise(Sequel::Error, "model object #{inspect} does not have a primary key") if opts.dataset_need_primary_key? && !pk
@@ -1062,13 +1082,14 @@ def _join_table_dataset(opts)
opts[:join_table_block] ? opts[:join_table_block].call(ds) : ds
end
- # Return the associated objects from the dataset, without callbacks, reciprocals, and caching.
- def _load_associated_objects(opts)
+ # Return the associated objects from the dataset, without association callbacks, reciprocals, and caching.
+ # Still apply the dynamic callback if present.
+ def _load_associated_objects(opts, dynamic_opts={})
if opts.returns_array?
- opts.can_have_associated_objects?(self) ? send(opts.dataset_method).all : []
+ opts.can_have_associated_objects?(self) ? _associated_dataset(opts, dynamic_opts).all : []
else
if opts.can_have_associated_objects?(self)
- send(opts.dataset_method).all.first
+ _associated_dataset(opts, dynamic_opts).all.first
end
end
end
@@ -1130,12 +1151,20 @@ def ensure_associated_primary_key(opts, o, *args)
end
# Load the associated objects using the dataset, handling callbacks, reciprocals, and caching.
- def load_associated_objects(opts, reload=false)
+ def load_associated_objects(opts, dynamic_opts=nil)
+ if dynamic_opts == true or dynamic_opts == false or dynamic_opts == nil
+ dynamic_opts = {:reload=>dynamic_opts}
+ elsif dynamic_opts.respond_to?(:call)
+ dynamic_opts = {:callback=>dynamic_opts}
+ end
+ if block_given?
+ dynamic_opts = dynamic_opts.merge(:callback=>Proc.new)
+ end
name = opts[:name]
- if associations.include?(name) and !reload
+ if associations.include?(name) and !dynamic_opts[:callback] and !dynamic_opts[:reload]
associations[name]
else
- objs = _load_associated_objects(opts)
+ objs = _load_associated_objects(opts, dynamic_opts)
run_association_callbacks(opts, :after_load, objs)
if opts.set_reciprocal_to_self?
if opts.returns_array?
@@ -1408,8 +1437,8 @@ def eager_graph_association(ds, model, ta, requirements, r, *associations)
if associations.first.respond_to?(:call)
callback = associations.first
associations = {}
- elsif associations.length == 1 && (assocs = associations.first).is_a?(Hash) && assocs.length == 1 && (pr, assoc = assocs.to_a.first) && pr.respond_to?(:call)
- callback = pr
+ elsif associations.length == 1 && (assocs = associations.first).is_a?(Hash) && assocs.length == 1 && (pr_assoc = assocs.to_a.first) && pr_assoc.first.respond_to?(:call)
+ callback, assoc = pr_assoc
associations = assoc.is_a?(Array) ? assoc : [assoc]
end
end
@@ -1609,9 +1638,8 @@ def eager_load(a, eager_assoc=@opts[:eager])
if associations.respond_to?(:call)
eager_block = associations
associations = {}
- elsif associations.is_a?(Hash) && associations.length == 1 && (pr, assoc = associations.to_a.first) && pr.respond_to?(:call)
- eager_block = pr
- associations = assoc
+ elsif associations.is_a?(Hash) && associations.length == 1 && (pr_assoc = associations.to_a.first) && pr_assoc.first.respond_to?(:call)
+ eager_block, associations = pr_assoc
end
if loader.arity == 1
loader.call(:key_hash=>key_hash, :rows=>a, :associations=>associations, :self=>self, :eager_block=>eager_block)
View
2  lib/sequel/plugins/identity_map.rb
@@ -121,7 +121,7 @@ def _associated_object_pk(fk)
# key option has a value and the association uses the primary key of
# the associated class as the :primary_key option, check the identity
# map for the associated object and return it if present.
- def _load_associated_objects(opts)
+ def _load_associated_objects(opts, dynamic_opts={})
klass = opts.associated_class
if klass.respond_to?(:identity_map) && idm = klass.identity_map and opts[:type] == :many_to_one and opts.primary_key == klass.primary_key and

Shouldn't dynamic_opts.empty? and be included here?

@jeremyevans Owner

No, but maybe we should add a !dynamic_opts[:callback]

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
opts[:key] and pk = _associated_object_pk(opts[:key]) and o = idm[klass.identity_map_key(pk)]
View
42 spec/integration/associations_test.rb
@@ -216,6 +216,48 @@ class ::Tag < Sequel::Model(@db)
@tag.reload.albums.should == []
end
+ specify "should handle dynamic callbacks for regular loading" do
+ @artist.add_album(@album)
+
+ @artist.albums.should == [@album]
+ @artist.albums(proc{|ds| ds.exclude(:id=>@album.id)}).should == []
+ @artist.albums(proc{|ds| ds.filter(:id=>@album.id)}).should == [@album]
+
+ @album.artist.should == @artist
+ @album.artist(proc{|ds| ds.exclude(:id=>@artist.id)}).should == nil
+ @album.artist(proc{|ds| ds.filter(:id=>@artist.id)}).should == @artist
+
+ if RUBY_VERSION >= '1.8.7'
+ @artist.albums{|ds| ds.exclude(:id=>@album.id)}.should == []
+ @artist.albums{|ds| ds.filter(:id=>@album.id)}.should == [@album]
+ @album.artist{|ds| ds.exclude(:id=>@artist.id)}.should == nil
+ @album.artist{|ds| ds.filter(:id=>@artist.id)}.should == @artist
+ end
+ end
+
+ specify "should handle dynamic callbacks for eager loading via eager and eager_graph" do
+ @artist.add_album(@album)
+ @album.add_tag(@tag)
+ album2 = @artist.add_album(:name=>'Foo')
+ tag2 = album2.add_tag(:name=>'T2')
+
+ artist = Artist.eager(:albums=>:tags).all.first
+ artist.albums.should == [@album, album2]
+ artist.albums.map{|x| x.tags}.should == [[@tag], [tag2]]
+
+ artist = Artist.eager_graph(:albums=>:tags).all.first
+ artist.albums.should == [@album, album2]
+ artist.albums.map{|x| x.tags}.should == [[@tag], [tag2]]
+
+ artist = Artist.eager(:albums=>{proc{|ds| ds.where(:id=>album2.id)}=>:tags}).all.first
+ artist.albums.should == [album2]
+ artist.albums.first.tags.should == [tag2]
+
+ artist = Artist.eager_graph(:albums=>{proc{|ds| ds.where(:id=>album2.id)}=>:tags}).all.first
+ artist.albums.should == [album2]
+ artist.albums.first.tags.should == [tag2]
+ end
+
specify "should have remove method raise an error for one_to_many records if the object isn't already associated" do
proc{@artist.remove_album(@album.id)}.should raise_error(Sequel::Error)
proc{@artist.remove_album(@album)}.should raise_error(Sequel::Error)
View
32 spec/model/associations_spec.rb
@@ -357,6 +357,28 @@ def ds.fetch_rows(sql, &block); MODEL_DB.sqls << sql; yield({:id=>234}) end
MODEL_DB.sqls.should == ["SELECT * FROM nodes WHERE (nodes.id = 234) LIMIT 1"]
end
+ it "should use a callback if given one as the argument" do
+ @c2.many_to_one :parent, :class => @c2
+
+ d = @c2.create(:id => 1)
+ MODEL_DB.reset
+ d.parent_id = 234
+ d.associations[:parent] = 42
+ d.parent(proc{|ds| ds.filter{name > 'M'}}).should_not == 42
+ MODEL_DB.sqls.should == ["SELECT * FROM nodes WHERE ((nodes.id = 234) AND (name > 'M')) LIMIT 1"]
+ end
+
+ it "should use a block given to the association method as a callback on ruby 1.8.7+" do
+ @c2.many_to_one :parent, :class => @c2
+
+ d = @c2.create(:id => 1)
+ MODEL_DB.reset
+ d.parent_id = 234
+ d.associations[:parent] = 42
+ d.parent{|ds| ds.filter{name > 'M'}}.should_not == 42
+ MODEL_DB.sqls.should == ["SELECT * FROM nodes WHERE ((nodes.id = 234) AND (name > 'M')) LIMIT 1"]
+ end if RUBY_VERSION >= '1.8.7'
+
it "should have the setter add to the reciprocal one_to_many cached association list if it exists" do
@c2.many_to_one :parent, :class => @c2
@c2.one_to_many :children, :class => @c2, :key=>:parent_id
@@ -1149,6 +1171,16 @@ class Value < Sequel::Model
v.model.should == Historical::Value
end
+ it "should use a callback if given one as the argument" do
+ @c2.one_to_many :attributes, :class => @c1, :key => :nodeid
+
+ d = @c2.create(:id => 1234)
+ MODEL_DB.reset
+ d.associations[:attributes] = []
+ d.attributes(proc{|ds| ds.filter{name > 'M'}}).should_not == []
+ MODEL_DB.sqls.should == ["SELECT * FROM attributes WHERE ((attributes.nodeid = 1234) AND (name > 'M'))"]
+ end
+
it "should use explicit key if given" do
@c2.one_to_many :attributes, :class => @c1, :key => :nodeid
Please sign in to comment.
Something went wrong with that request. Please try again.