Friday, August 28, 2009

has_many :through polymorphic fun

The only thing better than creating has_many :through relationships with extra attributes , is creating polymorphing relationships. It sounds a little complicated, but its a simple concept in practice.

Take the diagram below. A release can have many test cases in it, but it could also have bugs which need to be checked in the same release. Now we could create a join table between releases and cases as well as releases and bugs, or we could just use one join table that can link releases to both cases and bugs.



OK, time to install the plugin:
Installing using script/plugin install proved a problem, so you can also do this
Goto Github and download the plugin.

http://github.com/fauna/has_many_polymorphs/tree/master

You can then untar it, and move it to the folder

vendor/plugins/has_many_polymorphs


Now lets generate our models.
Keep an eye on the Releaseable model. It has only three columns
release_id - the release that the other models map to
runable_id - holds the id of the model that is joining to it
runable_type - holds the model name.
Using runable_id and runable_type, we know the model to look for, as well as the id of that model.


class Release < ActiveRecord::Base
has_many_polymorphs :runables, :from=>[:cases, :bugs], :through=>:releaseables
end

class Releaseable < ActiveRecord::Base
belongs_to :release
belongs_to :runable, :polymorphic=>true
end


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

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

create_table :bugs do |t|
t.string :title
t.text :description
end

create_table :releaseables do |t|
t.integer :release_id
t.integer :runable_id
t.string :runable_type # very important that this is a string!
end

end

def self.down
drop_table :releases
drop_table :cases
drop_table :bugs
drop_table :releasables
end
end


And that is it for a poylmorphic relationship. No methods need to be put in the Case or Bug models. So we can add as many relationships to releases by only editing the Release model.

Now lets use our new models. First we'll create our release, bug and case.

Release.create(:version=>1)
Bug.create(:title=>'Bug 1')
Case.create(:title=>'Case 1')
Case.create(:title=>'Case 2')


Joining them is as easy as using '<<'

Release.bugs << Bug.find(1)
Release.cases << Case.find(1)


If we look at our database now, we case see our runable_id, along with our runable_type so we know which table to look up to retrieve our record

+----+------------+------------+--------------+
| id | release_id | runable_id | runable_type |
+----+------------+------------+--------------+
| 1 | 1 | 1 | Bug |
| 2 | 1 | 1 | Case |
+----+------------+------------+--------------+


If we want to see what cases and bugs we have on a release, we can use

Release.find(:first).runable

to see all the bugs and cases.

Or, we can find the bugs or cases on their own

Release.find(1).bugs
Release.find(1).case


There is one other way to associate a case with a release, we can create a Releaseable record directly. This is really useful if we also want to set some extra attributes on the join table(Releaseable).

Releaseable.create(:release=> Release.find(:first), :runable=>Case.find(2))

Its as simple as that!

Sunday, August 16, 2009

has_many :through fun

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.

A simple test management system -



From the diagram, 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
end

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

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

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
end

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

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


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

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')