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!

No comments:

Post a Comment