The other ways :through

— March 28, 2006 at 15:47 PST


Rails 1.1 has finally been released, and it includes a bunch of yummy new features. Included in those are some changes to has_many :through associations that were added since I started writing about them.

The old stuff:

  • A has_many :through association lets you use a join model instead of a has_and_belongs_to_many association and a join table. The advantages are that you can easily read and update attributes on the association, and you can use eager loading (:includes) to pre-fetch associated model data.

The new stuff:

  • You can use the :through option on a has_many association without a join model. This allows you to collapse two has_many relationships into one.
  • Use the :source option instead of :class_name to specify the associated model when the default name isn't what you want.

The other kind of join model

In addition to reaching through a belongs_to/belongs_to join model, you can also use has_many :through where the join model is belongs_to/has_many. Here's an example:

class Meal < ActiveRecord::Base
  has_many :dishes
  has_many :ingredients, :through => :dishes
end

class Dish < ActiveRecord::Base
  belongs_to :meal
  has_many   :ingredients
end

class Ingredient < ActiveRecord::Base
  belongs_to :dish
end

If you want to go shopping for your meal, @meal.ingredients gets you your shopping list.

Use the :source

By default, a has_many :through association looks for an association in the join model with the same name as the base association. In our example above, the :ingredients looks for an :ingredients association in the Dish model. Sometimes that won't work, and in those cases use the :source option to specify which association to use.

class Meal < ActiveRecord::Base
  has_many :dishes
  has_many :ingredients, :through => :dishes
  has_many :meats,       :through => :dishes, :source => :ingredients,
                         :conditions => "ingredients.kind = 'meat'"
  has_many :veggies,     :through => :dishes, :source => :ingredients,
                         :conditions => "ingredients.kind = 'vegetable'"
  has_many :dry_goods,   :through => :dishes, :source => :ingredients,
                         :conditions => "ingredients.kind = 'dry'"
end

This is a change from using :class_name to specify the non-default model.

4 commentsassociations, rails

Comments
  1. Martijn2006-04-03 10:51:27

    How would I use this when having relations on the same table? With HABTM I would just use :foreignkey and :associationforeign_key like this:

    class User < ActiveRecord::Base hasandbelongsto_many :friends, :jointable => 'userfriends', :foreignkey => 'userid', :associationforeign_key => 'fr iendid', :classname => 'User' en

  2. Josh Susser2006-04-25 09:38:18

    @Martijn: check out my new article on self-referential has_many :through associations.

  3. Corey2006-10-22 21:41:52

    Great site, Josh!

    I just changed some join_table association to :through join model after reading all the associations-related articles here. Interesting thing:

    After adding the new :through associations, which fixed the pressing issue, some old accessor methods couldn't find the requested association via one of the models being joined. So I reverted to the join table, habtm, which allowed my accessor methods to find the association again. Luckily, the originally-broken method still worked, still using the new join model class to add to the join table! Pretty sweet, because update_attribute() puked every time I tried to update the join table by saving an object from one of the classes being joined.

    Anyway, cool site, very helpful -- thanks!

  4. Ben2006-11-14 17:22:29

    Great blog, seem to end up here for about 50% of my searches as I learn Rails.

    I'm wondering if there's a simple way to chain has_many :through relationships?

    In a project I'm working on we have Projects, CastingCalls, Roles, and Submissions. A Project has many CastingCalls, a CastingCall has many Roles, and a Role has many Submissions. I can easily get all the CastingCalls belonging to a Project via has_many :casting_calls, and via has_many :roles, :through => :casting_calls I can just as easily get all the Roles belonging to all the CastingCalls in the Project. I would like to be able to get all the Submissions for the entire Project though as well. My first guess was has_many :submissions, :through => :roles which expects that Roles are related directly to Projects (not through CastingCalls) and doesn't work. Along with a few other perversions and semi-random attempts at the has_many :through options I haven't been able to figure it out -- and just because Rails seems to have an exceedingly simple solution to everything I thought there might be one here as well.

Sorry, comments for this article are closed.