Has Many Through Association

December 6th, 2005

Yesterday, DHH posted a link to his Pursuit of Beauty slides from the Snakes and Rubies event this weekend.

I went through each of the slides looking for new stuff and found several great new things. If you look at slide 14, you’ll see a new :through parameter on one of the associations. There’s no documentation on this yet, but I did a little experimentation by checking out the latest edge_rails.

To see how this new type of association works, let’s look at the traditional way to handle many-to-many relationships when we want to store additional attributes about the join.

Lets take a simple example to illustrate how we can use the new functionality. Let’s say that we publish several newsletters and we let users sign up for as many of these newsletters as they want. We also need to to track several things about each subscription, such as the email format the user would like to receive that newsletter in. What we want to do in this case is make the join table a model. Let’s call it Subscription. We would have three tables: users, subscriptions, newsletters. The models would be set up like this:

:::ruby
class User < ActiveRecord::Base
  has_many :subscriptions
end

class Newsletter < ActiveRecord::Base
  has_many :subscriptions
end

class Subscription < ActiveRecord::Base
  belongs_to :user
  belongs_to :newsletter
end

Let’s say that we want to see a list of what newsletters John Doe has subscribed to:

:::ruby
@newsletters = []
User.find_by_name("John Doe").subscriptions.each do |s|
  @newsletters << s.newsletter
end

This works great, but it isn’t very elegant. It would be much nicer if we could just get all the newsletters without having to walk through the subscriptions.

Let’s add the :through associations to the models:

:::ruby
class User < ActiveRecord::Base
  has_many :subscriptions
  has_many :newsletters, :through => :subscriptions
end

class Newsletter < ActiveRecord::Base
  has_many :subscriptions
  has_many :users, :through => :subscriptions
end

class Subscription < ActiveRecord::Base
  belongs_to :user
  belongs_to :newsletter
end

Now we can just access the associated models directly:

:::ruby
@newsletters = User.find_by_name("John Doe").newsletters

There truly is beauty in simplicity.

Update I’ve updated this article with some better examples based on user feedback.