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.
I’m a bit mystified by the first example. The following would work just fine.
Maybe a different use case would enlighten me.
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).
labrat: Your code examples causes infinite recursion.
Dr Nic: Yes! Good post—I’d say Magic Models is enterprise ready.
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.
Chime in.