Abusing err.. Using AR::Associations
February 16, 2006 09:45 (Sydney Australia), updated 22 minutes later
In Francois Beausoleil’s recent post on Asymmetric Associations he showed the advantage of going with names and associations that are slightly unconventional (in the convention-over-configuration sense of the word).
Sometimes there is only so far convention can take you.
I think Francois is touching on a bigger issue that some Railer’s might not be aware of: ActiveRecord::Associations can be used to build almost all of your model associations, including the not-so-conventional ones.
For example, I’ve often seen code similar to the following:
class User < ActiveRecord::Base
has_many :emails
def unread_emails
Email.find_by_sql(["select * from #{Email.table_name}# where read = 0 and user_id = ?", self.id])
end
end
You can accomplish exactly the same thing by adding a second association (oops… now updated to include class_name):
class User < ActiveRecord::Base
has_many :emails
has_many :unread_emails, :class_name => 'Email', :condition => 'read = 0'
end
or, alternatively, by bolting on the unread method directly to the has_many association:
class User < ActiveRecord::Base
has_many :emails do
def unread; find(:all, :condition => 'read = 0'); end
end
end
Another great example comes from i2, the app that powers the rails wiki:
class Page < ActiveRecord::Base
belongs_to :book
has_many :versions, :order => "created_at", :dependent => true
has_one :current_version, :class_name => "Version", :order => "created_at DESC"
def find_or_build_version(number = nil)
number ? versions[number.to_i - 1] : versions.build(:body => body)
end
def body
current_version ? current_version.body : nil
end
def to_param
title
end
end
There’s a few cool tricks going on in this class, but for now we’ll just look at the current_version statement:
has_one :current_version, :class_name => "Version", :order => "created_at DESC"
Even though the Versions table has potentially thousands of records with the same page_id, the above line pulls out just one.
So why does this work, even though its pointing to a table with many version records?
has_one will take only a single record from the resulting query, similar to a find(:first), using a SQL limit clause.
For more neat tricks have a look at the rest of the i2 codebase—it’s a cool little app.


Comments
Jacek Becela
Is it possible to use these tricks with has_many :through assosciations in some way? (:class_name is ignored then)
I want something like this:
auction.cars auction.sold_cars auction.not_sold_cars and so on. Car is associated with Auction through Exposure join model (Car can be exposed on many auctions).
Tim Lucas
Good question… I haven’t really tried. Give it a try and report back!
rick
has_many :through associations are unique, since they read a source association for info. Use :source instead of :class_name or :foreign_key to configure it.
has_many :sold_cars, :through => :exposures, :source => :car, :conditions => ...
To comment on this article you must have javascript enabled.