activerecord has_many :through relationship

There are a good number of different join models with Rails, some more complicated than others.
One of my favorite is has_many :through, it lets you join two models together using a separate join table which can also hold extra attributes for that join.

So… a release has many test cases, but a test case has many releases. A simple HABTM join would almost do here, except, when a test case is part of a release it should have a result (PASS, FAIL, NOT_RUN etc). So a join table is needed to store these extra attributes.
We also want a test case to have the result of NOT_RUN when its add to a release.

So diving into the models while keeping an eye on the diagram above

class Release < ActiveRecord::Base
has_many :cases, :through=>runs
has_many :runs

class Case < ActiveRecord::Base
has_many :releases, :through=>runs
has_many :runs

class Runs < ActiveRecord::Base
belongs_to :case
belongs_to :release

Now for the migrations.
The interesting one is the ‘runs’ table. It contains an identifier to Release and another
one to Case, as well as a third column to store the result. The Release and Case objects dont need any extra identifiers, as its all managed in the Run table.

class SetItUp < ActiveRecord::Migration
def self.up
create_table :releases do |t|
t.string :version

create_table :cases do |t|
t.string :title
t.string :steps
t.string :expected

create_table :runs do |t|
t.integer :release_id
t.integer :case_id
t.string :result

def self.down
drop_table :releases
drop_table :cases
drop_table :runs

Next on the agenda – using them in the console. First lets create a release and two test cases, c1 and c2.

>> r = Release.create(:version=>1)
>> c1 = Case.create(:title=>’A Title’, :steps=>’A step’, :expected=>’Expected Stuff’)
>> c2 = Case.create(:title=>’Another Title’, :steps=>’Another step’, :expected=>’Expected Stuff’)

There are two ways to add a case to a release now. First lets use the << operator, which will automatically create the association between the release and the case.

>> r.cases << c1

Now if you look at the runs table in mysql, you will see release_id and case_id are both filled but the result is empty. Not exactly what we’re after.

| id | release_id | case_id | result | created_at | updated_at |
| 1 | 1 | 1 | NULL | 2009-08-16 15:51:34 | 2009-08-16 15:51:34 |

The next option is to explicitly set the release, the case and the result in the join.

>> Run.create(:release => r, :case => c, :result=>’NOT_RUN’)

Now if we take a look, we can see all the fields we want are filled! Great, now we can go on with testing our release.

| id | release_id | case_id | result | created_at | updated_at |
| 2 | 1 | 2 | NOT_RUN| 2009-08-16 15:53:15 | 2009-08-16 15:53:15 |

However, when we want to change the result in the Run table, we will have to reference the Run object directly.

>> Run.find(1).update_attributes(:result=>’PASS’)