Wednesday, January 16, 2008

Adding functionality to ActiveRecord and ActionController

Ruby gives you the ability to add methods to existing classes and in rails, this lets you add functionality to core classes. That way, you can use custom class methods in any Model, Controller, or View.

Let's say, I have an Address table in my DB. Addresses can belong to either a Person or Company. I add 2 columns to my Address table: owner_type
owner_id

So, my addresses will look something like this:

addressowner_typeowner_id
55 elm st.Person45
22 main st.Company99

I want to use functionality in other models too. So, I decide to put this functionality somewhere I can re-use it. First, I want to be able to add the columns to a table. I'm using rails migrations, so all I need to add some functionality to the base Table class.

First, I create a file /lib/poly_owner.rb module ActiveRecord
  module ConnectionAdapters
    class TableDefinition
      def has_polymorphic_owner
        column "owner_type", :string
        column "owner_id", :int
      end
    end
  end
end

this will add a method called 'has_polymorphic_owner' to the TableDefinition class. Once I do this, I can use this in my migration: /db/migrations/add_address.rb
create_table :addresses do |t|
  t.column :address, :string
  t.has_polymorphic_owner
end

Ok, so I have a nice way of adding columns. Now, I want to add some functionality to my model. It'd be nice if I could just say ... address.parent and get the object for that owner. I don't want to look up the owner_type every time. So, lets write some meta-functions...

In the same file: /lib/poly_owner.rb, I add module ActiveRecord
  class Base
    def self.has_polymorphic_owner
      #Get the parent object
      self.send(:define_method, :parent, Proc.new do
        klass = Kernel.const_get(self.owner_type)
        klass.find(self.owner_id)
      end)
    end
    def self.polymorphic_parent_to(child_name)
      self.send(:define_method, "first_#{child_name}".to_sym, Proc.new do
        childKlass = Kernel.const_get(child_name.to_s.capitalize)
        childKlass.find(:first, :conditions => "owner_type = '#{self.class.to_s}' and owner_id = #{self.id}")
      end)

      self.send(:define_method, "all_#{child_name}".to_sym, Proc.new do
        childKlass = Kernel.const_get(child_name.to_s.capitalize)
        childKlass.find(:all, :conditions => "owner_type = '#{self.class.to_s}' and owner_id = #{self.id}")
      end)

      #This will delete the children before deleting the parent.
      destroy_method = "destroy_#{child_name}".to_sym
      before_destroy destroy_method
      self.send(:define_method, destroy_method, Proc.new do
        child = instance_variables_get("all_#{child_name}")all_address
        child.destroy if child_name
      end)

    end
  end
end

This did a couple of interesting things, I defined 2 main methods: has_polymorphic_owner and has_polymorphic_child. These are both class files I can call inside my models. When these methods are called, they define other methods in that class. So, now my models can look like this: class Address < ActiveRecord::Base
  has_polymorphic_owner
end

class Person < ActiveRecord::Base
  polymorphic_parent_to :Address
end

Then i can say things like:
Address.find(1).parent
Person.find(1).all_address
Person.find(1).destroy (This will delete all the associated Addresses too)

No comments: