toolmantim

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.