Nesting your has_many :through relationships


View the Source Code

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.

About these ads

Tags: , , , , , , ,

2 Responses to “Nesting your has_many :through relationships”

  1. Jarl Friis Says:

    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

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s


Follow

Get every new post delivered to your Inbox.

%d bloggers like this: