Thursday, June 12, 2008

Polymorphic Raccoon


Yesterday night, my mom was terribly scared by a raccoon. She said it made a funny face when she was watching TV.

This brought a question to me. Is raccoon offensive? If not, why was my mom scared?

So, if I create an label and attach it to anything (I mean anything, not just animals) which are offensive, my mom should NOT be scared.

Since the label is going to be attached to ANYTHING, it brings me an association dilemma. If a Offend label is attached to Raccoon class, an association from Raccoon to Offend is established. If another Offend label is attached to Dog class, another association to Offend is also established. So, Offend must hold both 'foreign keys' to Man and Dog. But each record of Offend class always has one foreign key with Null value. For example, if Offend record 1 points to Raccoon, the foreign key to Dog must be Null. Vise versa.

In Ruby On Rails, 'polymorphic has_many association' can solve the problem. 'polymorphic has_many association' hooks a particular class to any kind and number of sources classes. These source classes may not be related.

First, let's define an INTERFACE called offensive (you can use any name) and declare it is polymorphic.

Second, let's create a Offend class which will be attached as a label to other classes. Then, we make Offend class belong to the INTERFACE.

offend.rb
class Offend < ActiveRecord::BASE
belongs_to :offensive, :polymorphic => true
end


Third, create two columns in table offends: offensive_id and offensive_type

002_create_offends.rb
class CreateOffends < AcriveRecord::Migration
self.up
create_table :offends do |t|
t.column :offensive_level, :integer
t.column :offensive_id, :integer
t.column :offensive_type, :string
t.timestamps
end

self.down
drop_table :offends
end
end


In Rails 2.0+, a new macro style method 'references' is introduced to automatically create the INTERFACE's id and type.

002_create_offends.rb
class CreateOffends < AcriveRecord::Migration
self.up
create_table :offends do |t|
t.column :offensive_level, :integer
t.references :offensive, polymorphic => true
t.timestamps
end

self.down
drop_table :offends
end
end


But I found only offensive_id was generated, not offensive_type. The INTERFACES's type field is used to hold the classes' type of which the Offend class will be attached to, such as Raccoon or Dog. So more research on 'references' method is needed in the future.

Now we are ready to attach this class to any source classes.

First, simply declare has_many offends via offensive INTERFACE.
dog.rb
class Dog < ActiveRecord::BASE
has_many :offends, :as => :offensive
...
end

raccoon.rb
class Raccoon < ActiveRecord::BASE
has_many :offends, :as => :offensive
...
end


Second, create a Offend object and update its :offensive attribute by passing the source objects (like instance of Dog or Raccoon) in.


>>d = Dog.find :first
>>r = Raccoon.find :first
>>o1 = Offend.new offensive_level => 3
>>o2 = Offend.new offensive_level => 5
>>o1.update_attribute(:offensive, d)
>>o2.update_attribute(:offensive, r)



Notice, you can't do o1.save. It won't work. o1.update_attribute(:offensive, d) will automatically save the newly created o1 into database and set o1.offensive_type to Dog.


Now we can see, by attaching the class Offend, Raccoon is more dangerous then Dog.

No comments: