Posted on December 6, 2005 at 6:20 pm

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:

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:

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

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:

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

15 Comments:
Posted on December 7, 2005 at 7:14 am by Nuby

In the last example, how does “u” from the outer block become visible in the inner block? is it just normal scoping in Ruby?

This through thing is brilliant though, and thanks for an excellent example, very helpful for the likes of moi! :)

sorry for the stupid question. N00b here.

Posted on December 7, 2005 at 9:04 am by Sean

Nuby,

The scope from the outer block is included in the inner block. If you’re familiar with Java, C or C++ think of the inner block as a for loop:

User u = myUser;
for (int i = 0; i < newsletters.length; i++) {
  Newsletter n = newsletters[i];
  n.mail(u.email(), u.email_format());
}

Posted on December 7, 2005 at 11:04 am by Admin

Ruby blocks inherit variables from the surrounding scope. They even have access to those variables when the original scope no longer exists.

For example:

class FirstClass
  def create_proc
    abc = ("a".."z").to_a
    Proc.new {abc.each {|alpha| puts alpha}}
  end
end

f = FirstClass.new
p = f.create_proc
f = nil
p.call

The output is:
a
b
c
d
e
f
g
h
i
j
k
l
m
n
o
p
q
r
s
t
u
v
w
x
y
z

Posted on December 24, 2005 at 9:52 pm by Simon Harris

I must be missing something because it seems to me at least that the original example could be re-written first as as:

@users = User.find :all, :conditions => “name like ‘k%’�
@users.each do |u|
for subscription in u.subscriptions
subscription.newsletter.mail(u.email, subscription.email_format)
end
end

and then:

@users = User.find :all, :conditions => “name like ‘k%’�
@users.each do |u|
for s in u.subscriptions
s.newsletter.mail(u.email, s.email_format)
end
end

and finally:

@users = User.find :all, :conditions => “name like ‘k%’�
@users.each do |u|
u.subscriptions.each do { |s| s.newsletter.mail(u.email, s.email_format)
end

Making it identical to the second example that uses :through.

If this is correct, then It’s not clear to me what exactly :through is giving me. I’m sure it’s great, but I’ve obviously missed something in your example.

Cheers,

Simon

Posted on December 25, 2005 at 3:38 pm by Admin

Simon,

You’re right. In my rather contrived example it would be better to use the following code:

@users = User.find :all, :conditions => "name like 'k%'"
@users.each do |u|
  u.newsletters.each do |n|
    n.mail(u.email,n.subscription.mail_format)
  end
end

I am using the through associations in several places in one of my projects to good effect. When I get some time, I’ll post another entry that’s based on some real-world usage.

Posted on December 25, 2005 at 4:04 pm by Simon Harris

Excellent. I look forward to it.

Cheers,

Simon

P.S. Apologies for the unformatted code. It was late and I didn’t notice the list of allowable tags below :)

Posted on December 31, 2005 at 1:30 am by Peter Morris

Hmmm, the original code, before the “through” could be re-written like……

@users = User.find :all, :conditions => “name like ‘k%’�
@users.each {|u| u.subscriptions.each{|s| s.mail(u.email,s.email_format)}}

you could even get rid of @users and have the .each hanging off of the find.

You COULD do sommat like….

@users = find … blah blah …
@users.collect{|u| u.subscriptions.collect{|s| [u, s]}}.each{|t| t[1].mail(t[0].email, t[1].email_format)}

But it would be a bad idea, as you would be building an in-memory array with as many rows as you have subscriptions whos subscribers name begins with ‘k’.

Posted on February 27, 2006 at 10:17 pm by Randy Schmidt

I think I really need this for a site I’m working on, however, I need it for a self-referential many-to-many relationship and I have had no luck getting it to work. Does anybody know how I could make this work?

Posted on March 14, 2006 at 1:46 am by Edoardo "Dado" Marcora

I also need it for a self-referential many-to-many relationship and I also can’t get it to work.

Posted on April 12, 2006 at 7:52 pm by BeepHONK rumbles to life… at 72mach1.com

[...] I’ve got most of the initial featureset roughed in. It’s even got some sprinklings of AJAX and DOM JavaScript effects. I’ve got a problem with the :through relationships that came about somewhere between the 1.0 and 1.1 release of Rails. Never fear though I’ll get it working soon enough. [...]

Posted on July 21, 2006 at 12:14 pm by Mike at SEOG.net

What is helpful about it is the additional aspects you can add when you flush out the model like that. You can say when a subscription was created, who created it, and so forth. By making the model of subscriptions it is easier to add functionality that is often wanted by marketing and business folks. Although he was talking about cases where the subscription model wasn’t defined, where it was simply a many to many relationship. Joining things with the has many :through and creating a new model allows one to stay “CRUD”-y and the benefits that comes along with that.

[...] Links zum Thema: rubyonrails.com::wiki::ThroughAssociations matthewman.net/…/activerecord-goes-through api.rubyonrails.org/…/ActiveRecord/…/ClassMethods wiki.rubyonrails.org/…/PolymorphicAssociations infused.org/…/has-many-through-association gmane.comp.lang.ruby.rails blog.hasmanythrough.com/…/associations blog.hasmanythrough.com/../many-to-many-dance-off wiki.rubyonrails.org/…/Beginner+Howto+on+has_many+through [...]

[...] Die habtm-Funktionalität in Rails ist für n:m-Beziehungen gedacht. Was aber, wenn jede dieser Beziehungen zusätzliche Attribute mit sich bringen soll? Beispiel: ein User soll Teil verschiedener Gruppen sein können und einer Gruppe sollen mehrere User angehören können. Um dies umzusetzen, ist die habtm-Beziehung perfekt. Nun soll aber zusätzlich jeder User einer Gruppe eine Rolle inne habe, z.B. User 1 hat in Gruppe 1 die Rolle “admin”, in Gruppe 2 “member” etc. Hierfür bietet sich sich eine “has_many through” an. Weiteres unter http://www.infused.org/2005/12/06/has-many-through-association/ [...]

Posted on December 26, 2007 at 11:00 am by Poli

Hello, I want to know if that is good for my aplicattion, I have three tables in mysql;
the first is Person, the second is Friend, and the third is persons_friends

These tables has :
has_and_belong_to_many for relationship

Then, I have a fourth table that is related with persons_friends;
I don’t know how to do a relationship between the fourth table and persons_friends

Poli

Posted on September 12, 2008 at 4:57 am by Nitish

Hi, Can you please post an example of how I can create a User, a Newsletter and a Subscription all at the same time?
Is it needed to have a Subscriptions controller for this?

Thanks,
Nitish