Skip to navigation

Abusing err.. Using AR::Associations

Published February 15, 2006 - Updated February 15, 2006

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.

Archived comments

Comments were previously allowed on articles. Though no new comments are being accepted you can see the old comments below.

  1. 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).

  2. Tim Lucas

    Good question… I haven’t really tried. Give it a try and report back!

  3. 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 => ...

More reading…

Previously: For the love of Gates!

Next up: Site error notification in Farcry 3

Articles

toolmantim

I'm Tim Lucas, a user experience designer and developer currently in Sydney Australia.

I run a web application design and development company Agency Rainford, present on various technical topics, snap the occasional photo, tweet my going-ons, share teh codes and post other tid-bits to the tumble.

Most recently I published LAN hacking with Bananajour (June 11, 2009)

Shoot an email to and say hello.

Powered by faux western Buddhism