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.
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))
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
@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 :)
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.