Thursday, December 28, 2006

has_many :through associations and the push (<<) operator.

Background:
I needed to create a many-to-many association between two of my models, called Band and Show, i.e. a band can be a part of many shows, and many shows have bands performing. I considered the traditional method of using a has_and_belongs_to_many (habtm) association and creating a "bands_shows" join table. However, the associations themselves needed to contain their own set of information. This is, of course, still possible with a habtm association. Unfortunately, it's more of a hack than an elegant solution.

This is where has_many :through associations come in. has_many :through is a method for creating a many-to-many association between two models and contain a set of information within themselves. There is sufficient documentation of it through Google already, so I won't go over all the details. For a comparison between habtm and has_many through association, visit http://weblog.rubyonrails.org/2006/04/21/habtm-vs-has_many-through/

Problem:
For my project, I mentioned above that the two models I was trying to associate were Show and Band. I decided to call their associating join table "Performance," since a band's relationship to a show is called a performance. I set up my database and Rails models, and everything was going great... until I tried to use the push operator, e.g.
Show.find(:first).bands << Band.find(:first)
This is where my world came crashing down. Since, at this point, I've only used habtm associations, I was expecting to be able to add these relationships just as easily as using a push operator. Oh, how wrong was I. A Google search then revealed to me that the push operator does not work with has_many :through associations. SADFACE, I thought. Will I have to resort to creating a method in the Show model called add_band and a method in the Band model called add_show? No, that's ugly. I refuse.

Solution :)
A little more research then revealed that a push operator solution was already in the works and was implemented in Rails edge. Unfortunately, I couldn't (or didn't want to) upgrade to edge, so I decided to implement my own push method and remove it when Rails stable implemented it, thereby not having to edit any of the method calls.

Band.rb:

class Band < ActiveRecord::Base
has_many :shows, :through => :performances, do
def <<(show)
begin
Performance.create!(:show => show, :band => @owner)
self.concat([show])
rescue
nil
end
end
end
end

Pretty simple, isn't it? What's important to note is @owner. Rails is a little wonky with the way it handles closures, so when you're in the has_many :shows block, "self" actually references the association array, rather than the Band object. In any case, there it is. Here are the other two models, just in case you need them.

Show.rb:

class Show < ActiveRecord::Base
has_many :bands, :through => :performances do
def <<(band)
begin
Performance.create!(:show => @owner, :band => band)
self.concat([band])
rescue
nil
end
end
end
end


Performance.rb:

class Performance < ActiveRecord::Base
belongs_to :band
belongs_to :show
end