4
votes

I wrote a convenience ActiveRecord extension to delegate methods to a base object (based on multi-table inheritance)

class ActiveRecord::Base
  def self.acts_as(base)
    class_eval %Q{
      def method_missing(method, *args, &blk)
        #{base}.send(method, *args, &blk)
      rescue NoMethodError
        super
      end
    }
  end
end

I have a state class and a base class

# state class
class MyState < ActiveRecord::Base
  belongs_to :my_object
  acts_as :my_object
end

# base class
class MyObject < ActiveRecord::Base
  has_one :head, :class_name => 'MyState'
  has_one :tail, :class_name => 'MyState'
end

When I tried this out, I found out that it doesn't work in some cases. More specifically,

> MyState.first.some_method_in_base
nil
> MyObject.first.tail.some_method_in_base
NoMethodError: undefined method `some_method_in_base' for #<ActiveRecord::Associations::HasOneAssociation:0xABCDEFG>

Can anyone enlighten me as to why one works and the other doesn't?

1
I'd also suggest using the ActiveSupport provided version of this, which is called #delegate: api.rubyonrails.org/classes/Module.html#M000051François Beausoleil
Yep, I ended up using delegates, but they aren't as convenient as the concept of forwarding all missing methods to a base class.Yitao

1 Answers

5
votes

When you run MyObject.first.tail the object that actually responds is an AssociationProxy class that

has most of the basic instance methods removed, and delegates # unknown methods to @target via method_missing

You can get more details about the proxy running:

MyObject.first.proxy_owner
MyObject.first.proxy_reflection
MyObject.first.proxy_target

If you look in the code, you can see that the AssociationProxy proxies the method some_method_in_base to your MyState class only if MyState responds to some_method_in_base as you can see in the code below.

  private
    # Forwards any missing method call to the \target.
    def method_missing(method, *args, &block)
      if load_target
        if @target.respond_to?(method)
          @target.send(method, *args, &block)
        else
          super
        end
      end
    end

Therefore, the method_missing you have defined in the target class is never called.