Let’s say you’re creating a site where people can track their memberships in various store clubs – from grocery store loyalty cards, to memberships to Sam’s or CostCo. People and Stores have a many-to-many relationship called a Membership. Stores also have sales, and you want people to be able to manage all sales at all of their stores easily.
The Problem
You’d like to be able to say @member.sales, but there’s a problem – Rails doesn’t support daisy-chaining associations the way we’d like. Here’s how we want to setup our associations:
# app/models/member.rb class Member < ActiveRecord::Base has_many :memberships has_many :clubs, :through => :memberships has_many :sales, :through => :clubs end
# app/models/club.rb class Club < ActiveRecord::Base has_many :memberships has_many :members, :through => :memberships has_many :sales end
# app/models/membership.rb class Membership < ActiveRecord::Base belongs_to :member belongs_to :club end
# app/models/sale.rb class Sale < ActiveRecord::Base belongs_to :club end
And for reference, here’s the full schema:
# db/schema.rb
ActiveRecord::Schema.define(:version => 20100129152803) do
create_table "clubs", :force => true do |t|
t.string "name"
t.datetime "created_at"
t.datetime "updated_at"
end
create_table "members", :force => true do |t|
t.string "name"
t.datetime "created_at"
t.datetime "updated_at"
end
create_table "memberships", :force => true do |t|
t.integer "member_id"
t.integer "club_id"
t.date "expires"
t.datetime "created_at"
t.datetime "updated_at"
end
create_table "sales", :force => true do |t|
t.string "name"
t.text "description"
t.date "start"
t.date "end"
t.integer "club_id"
t.datetime "created_at"
t.datetime "updated_at"
end
end
While everything looks good from a data modeling perspective, there’s one issue – our member model isn’t allowed to daisy-chain assocations, so we can’t get to our sales easily. A call to @member.sales gives us this:
ActiveRecord::StatementInvalid: SQLite3::SQLException: no such column: clubs.member_id: SELECT "sales".* FROM "sales" INNER JOIN "clubs" ON "sales".club_id = "clubs".id WHERE (("clubs".member_id = 1))
The Solution
In comes Ian White’s nested_has_many_through plugin, which does exactly what you’d think Rails does already. Without changing the way you create associations, this plugin “just works” out of the box. Nothing to include in models or config files. Here’s how you install it:
script/plugin install git://github.com/ianwhite/nested_has_many_through.git
Now run @member.sales and you get what you’d expect – a list of all sales that a member is entitled to attend. You can nest even deeper if you like, but I offer this word of caution. Nested has_many :through associations are like the most precious liquid on earth: Captain Morgan’s spiced rum. Enjoy in moderation :)
Epilogue
I’ve created a full rails app on GitHub (View the Source Code) so you can download and play around with it. There are “before” and “after” tags, so you can see how the app reacts with and without the plugin. It also has Shoulda tests.
Tags: associations, developers, has_and_belongs_to_many, has_many, has_many :through, Rails, Ruby, ruby on rails
March 24, 2010 at 2:23 am |
Perfect!!! It just turned my tests (which I wrote prior to code) to green. Working out of the box. There are other people having run into the same problem: http://stackoverflow.com/questions/2383479/ruby-on-rails-multiple-has-many-through-possible
I really wonder why this lacking feature in standard rails is not considered a bug. Someone should really report it as a bug.
Thanks a lot for the plugin… It keeps my code clean and DRY.
Jarl
March 25, 2010 at 7:35 am |
Thanks Jarl, but I do want to give credit where credit is due. In case there’s any confusion, Ian White wrote the plugin, and I just wrote this little how-to :)