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 ahas_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 ahas_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.
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
@Martijn: check out my new article on self-referential
has_many :through
associations.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!
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 viahas_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 washas_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 thehas_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.