Err the Blog Atom Feed Icon
Err the Blog
Rubyisms and Railities
  • “Accessor Missing”
    – Chris on August 22, 2006

    A recent thread on the Rails mailing list grappled my attention: how do association accessor methods work, how do attribute accessor methods work, and what’s the difference (in ActiveRecord)?

    Association accessor methods are things like story.author when Story belongs_to :author. An attribute accessor method would be something like story.title when the stories database table has a title column. You’re familiar, I assume.

    The question arose from a failed attempt to override an association accessor method. Something like this did not work:

    class Story < ActiveRecord::Base
      belongs_to :author
    
      def author
        auth = super
        auth.name
      end
    end
    

    Obviously I want stories.author to return the name of a particular story’s author rather than the associated Author object. Remember that the super method calls the current method on the class parent. I’m trying to call the author method, then, on ActiveRecord::Base (hoping it will return the associated author object like normal). Rails, unfortunately, is having none of it:

    >> story = Story.find(:first)
    => #<Story:0x279f9e0 ...>
    >> story.author
    NoMethodError: super: no superclass method `author'
    

    Alright, I should have seen that coming. There’s this method_missing magic all over ActiveRecord… maybe author is a magical method? If that’s the case a question becomes apparent: if my parent class doesn’t have the method I’m calling super from, will its method_missing be hit? Let’s see.

    $ cat animals.rb
    class Animal
      def method_missing(method, *args)
        "Sorry, I don't have #{method}."
      end
    end
    
    class Dog < Animal
      def bark
        super
      end
    end
    
    tanner = Dog.new
    puts tanner.bark
    

    I expect to get a super: no superclass method `bark’ error. After all, isn’t this basically the same situation I’m running into with my authors method?

    $ ruby animals.rb
    Sorry, I don't have bark.
    

    Interesting. So super did work as expected and hit method_missing. What’s really going on here?

    The truth’s that authors is not really a method_missing trick. It’s a real method defined on my Story class. Where does it get defined? When belongs_to is called. Like much of Rails’ sugar, belongs_to is a class method of ActiveRecord::Base. When you call it in a subclass, stuff happens.

    The adventurous can peek into active_record/associations.rb around line 646 to see how belongs_to is defined. Here’s the cliffs: when belongs_to is called, ActiveRecord defines reader and writer methods on the calling class. Now it’s starting to make sense: author is added to my Story class when belongs_to is called. It’s not a method_missing trick and doesn’t exist on ActiveRecord::Base. Knowing this, how do we get our original example to work?

    Enter: alias. You may have seen it lurking around, all meta-like and somewhat shady. alias, more or less, copies a method. That’s it. It’s useful when you want to override a method but still keep the original method around. The technique is sometimes called “method chaining.” Say it: “meth-odd-chain-ing.”

    Solution:

    class Story < ActiveRecord::Base
      belongs_to :author
    
      alias :real_author :author
      def author
        auth = real_author
        auth.name
      end
    end
    

    Now?

    >> story = Story.find(:first)
    => #<Story:0x279f9e0 ...>
    >> story.author
    => "Christoe Jefferson"
    

    That feels great. But I still have a question: what about, say, story.title? Does ActiveRecord define all of my attribute accessor methods in the same way it defines my association accessor methods?

    All told, attribute accessor methods are indeed method_missing magic. Get pumped: super will work when overriding them.

    class Story < ActiveRecord::Base
      belongs_to :author
    
      def title
        "This story's title is: " + super
      end
    end
    

    So close, so close…

    >> story = Story.find(:first)
    => #<Story:0x279f9e0 ...>
    >> story.title
    => "This story's title is: Accessor Missing"
    

    Paydirt.

    That overachiever Josh Susser has a great write up on how Rails uses method_missing to allow dynamic finders, like Story.find_by_title(‘Accessor Missing’). Check it out if you haven’t already. There’s also that dude who can’t seem to keep those eels out of his hovercraft—he’s got a great post on using alias.

  • labrat, about 2 hours later:

    I’m a bit mystified by the first example. The following would work just fine.

    def author
    self.author.name
    end

    Maybe a different use case would enlighten me.

  • Dr Nic, about 1 hour later:

    Good article. Because the associations are generated functions, it allows the Magic Models to be as efficient as a normal call to an explicit association (apart from the one-time cost per-association of discovery and setup).

  • Chris, about 3 hours later:

    labrat: Your code examples causes infinite recursion.

    Dr Nic: Yes! Good post—I’d say Magic Models is enterprise ready.

  • Gabe, 5 days later:

    Wow, this post really helped me out a lot. I am creating a site for a brick and mortar music retailer with a legacy (Access!) database. The catalog has a string field for Artist, but on the website they create special artist pages which require a full model. It doesn’t make sense to automatically generate Artists based on the single field from the legacy database for several reasons. One is that artists may be spelled differently, and another is that it would result in incomplete artist records that would complicate the issue of display Artist pages.

    So you can see where this is going. My Album model has both an :artist association and an :artist attribute. Using this information I aliased the generated method as :artist_model (which is used less) and redefined :artist to call super.

    It’s an elegant solution to an ugly problem. God I feel sorry for those folks who think Rails == scaffold. Try doing something even remotely like this in Java or PHP. Ruby is amazing.

  • Four people have commented.
    Chime in.
    Sorry, no more comments :(
This is Err, the weblog of PJ Hyett and Chris Wanstrath.
All original content copyright ©2006-2008 the aforementioned.