Sometime ago while using method_missing to implement some functionality I got the weird behavior that it would work only most of the time but not always.
In retrospect it's now pretty obvious but in the heat of the moment it took me about half a day of investigation and talking before I figured it out.
What happened is that I did only half of the work.
I defined method_missing but I forgot to define respond_to? accordingly.
The result is that it worked when I called it directly on the instance, but failed if an association was involved.
To give an example, say you have a class like this:
class A < ActiveRecord::Base def example true end def method_missing(method, *args) if method.to_s =~ /example/ example else super end end end
Calling *example* directly on your instance works just fine.
>> A.new.my_example
=> true
>> A.create!.example_me?
=> true
All fine, but as soon as you get an association in the middle of things:
class B < ActiveRecord::Base belongs_to :a end
It just doesn't go well anymore:
>> b = B.new(:a=>A.new)
=> #<B id: nil, a_id: nil, created_at: nil, updated_at: nil>
>> b.a.example?
NoMethodError: undefined method `example?' for #<A id: nil, created_at: nil, updated_at: nil>
from /var/lib/gems/1.8/gems/activerecord-2.3.5/lib/active_record/associations/association_proxy.rb:220:in `method_missing'
from (irb):55
>> b = B.create!(:a=>A.create!)
=> #<B id: 4, a_id: 7, created_at: "2010-03-08 21:15:01", updated_at: "2010-03-08 21:15:01">
>> b.a.failing_example
NoMethodError: undefined method `failing_example' for #<ActiveRecord::Associations::BelongsToAssociation:0xb70491d0>
from /var/lib/gems/1.8/gems/activerecord-2.3.5/lib/active_record/associations/association_proxy.rb:220:in `method_missing'
from (irb):57
Now, this last error is a bit clearer but I don't remember running into it at the time.
If I had just followed the association_proxy:220 hint right away... ;)
What happens is that b.a doesn't return the instance but rather an AssociationProxy instance that provides ActiveRecord's extended functionality and this proxy relies on A#respond_to? to correctly forward method calls to the actual instance.
What I should have done is:
class A < ActiveRecord::Base def example true end def method_missing(method, *args) if method.to_s =~ /example/ example else super end end def respond_to?(method, include_private = false) if method.to_s =~ /example/ true else super end end end
>> B.create!(:a=>A.create!).a.example?
=> true
>> B.new(:a=>A.new).a.failing_example
=> true
There are some much better write-ups on this topic, if you want to read more:
Using method_missing and respond_to? to create dynamic methods
Solving the method_missing/respond_to? problem
Now, I must be honest here: what I was doing was a big code smell :)
It taught me the lesson to use method_missing properly and was even quite fun to debug and all that but what I really needed and end up doing in that case was a group of delegates here and there and voilĂ , it was all cool and clean.
Labels: meta programming, ruby on rails
0 Comments:
Subscribe to:
Post Comments (Atom)