I did not like the way that Rails did polymorphic associations. I found it impossible to add constraints into the database itself to make sure that things were hooked up like they were suppose to be. And doing the validation of all the if's, and's, and but's in Rails appeared to me would lead to very expensive validations.
I also did not like the concept behind single table inheritance. I find that whole concept extremely lame.
Mostly from ideas I got from the Postgres mailing list, I implemented another way to do polymorphic associations. It seems to be working for me. I thought I would share it. There are definitely some rough edges but those are not getting in my way right now. I've managed to tip toe around and get this to work with no modifications to Rails and only a small amount of work (once the tricks are figured out).
The polymophic base class in this example I call ItemBase (with a table of item_bases). The migration looks like this:
class CreateItemBases < ActiveRecord::Migration def self.up create_table :item_bases, :id => false do |t| t.integer :item_id, :null => false t.string :item_type, :null => false t.timestamps end execute "ALTER TABLE item_bases ADD CONSTRAINT fk_item_bases_item_type FOREIGN KEY (item_type) REFERENCES item_types(class_name)" execute "CREATE OR REPLACE FUNCTION item_id_test(item_id INTEGER, item_type TEXT) RETURNS BOOLEAN AS $$ DECLARE tn TEXT; qry TEXT;
BEGIN SELECT INTO tn table_name FROM item_types WHERE class_name = item_type; IF NOT FOUND THEN RETURN FALSE; END IF; qry = 'SELECT item_id FROM ' || quote_ident(tn)
' AS tn ' ||
'WHERE tn.item_id = ' || item_id::text || ';'; EXECUTE qry; IF NOT FOUND THEN RETURN FALSE; END IF; RETURN TRUE; END; $$ LANGUAGE plpgsql;" execute "ALTER TABLE item_bases ADD CONSTRAINT ck_item_bases_item_id CHECK (item_id_test(item_id, item_type))" execute "ALTER TABLE item_bases ADD CONSTRAINT key_item_id UNIQUE (item_id)" execute "ALTER TABLE item_bases ADD CONSTRAINT key_item_tuple UNIQUE (item_id, item_type)" execute "CREATE SEQUENCE item_bases_item_id_seq" end
def self.down drop_table :item_bases execute "DROP SEQUENCE item_bases_item_id_seq" execute "DROP FUNCTION item_id_test(INTEGER, TEXT)" end end
The model looks like this:
class ItemBase < ActiveRecord::Base set_primary_key "item_id" belongs_to :item, :polymorphic => true
def id item_id end end
In this example, the subclasses can be companies, people, etc. The migration to create the companies table is:
class CreateCompanies < ActiveRecord::Migration MY_CLASS_NAME = "Company" MY_TABLE_NAME = "companies"
def self.up create_table MY_TABLE_NAME, :id => false do |t| t.integer :item_id, :null => false t.string :item_type, :null => false, :default => MY_CLASS_NAME t.timestamps