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
#
# Attributes start here
#
t.string :name, :null => false
end
execute "ALTER TABLE #{MY_TABLE_NAME} ALTER COLUMN item_id
SET DEFAULT
nextval(''item_bases_item_id_seq'')"
execute "ALTER TABLE #{MY_TABLE_NAME} ADD CONSTRAINT
#{MY_TABLE_NAME}_item_type
CHECK (item_type = ''#{MY_CLASS_NAME}'')"
execute "ALTER TABLE #{MY_TABLE_NAME} ADD CONSTRAINT
fk_#{MY_TABLE_NAME}_item_id
FOREIGN KEY (item_id, item_type)
REFERENCES item_bases(item_id, item_type)
ON DELETE CASCADE INITIALLY DEFERRED"
ItemType.create(:table_name => MY_TABLE_NAME, :class_name =>
MY_CLASS_NAME)
end
def self.down
drop_table MY_TABLE_NAME
ItemType.destroy_all(:table_name => MY_TABLE_NAME)
end
end
I have not written a generator yet but instead I create a couple of
constants and then the rest of the migration is the same except for
the subclass specific fields.
The model is:
class Company < ActiveRecord::Base
set_primary_key "item_id"
set_sequence_name "item_bases_item_id_seq"
has_one :item_base, :as => :item
# subclass specific validations
validates_presence_of :name
end
First, as you can see, most of the migration is introducing
constraints into the database. These are PostgreSQL specific. For
other databases, there are probably similar concepts. And for those
who do not believe in db constraints, just don''t do them.
In the constraints, another table is referenced: item_types. Its
associted class is ItemTypes. It simply maps table names to class
names for the db to use. As part of the migration for each subclass,
I add in the needed row into the item_types table.
In brief, for item_bases, the id is constrainted to be unique. I also
constrain the ( id, type) tuple so I can use it as a foreign key. The
item_id_test verifies that an entry id and type are in the proper
table. Going the other direction, the companies table has a deferred
constraint that a matching entry is in item_bases. At the time of the
commit, both entries must be there. There is no way for duplicates or
other weird mixtures. These same constraints could be lifted up into
Rails if that is the user''s choice.
The main difference between this style and the normal Rails style of
polymophic association is that the id and type are in both the base
and the subclass. The base does not have its own id. So, with an id,
you can find the base instance. From the base instance, you can find
the subclass instance. Also, from the subclass instance, you can find
the base instance. This is roughly the same as normal Rails -- the
only difference is the id is the same for both the base instance and
the subclass instance. (The type is the same too.)
I find this works well for me. It is robust and somewhat fool proof
once it is set up. There is nothing here that could not be
generalized so a generator could be written to create the base class
and a second generator that would create each subclass passing in the
name of the base class.
Someone has mentioned that this can not be extended. i.e. the base
class could not itself be a subclass of another class. But I don''t
understand why not. I have not tried it so I can''t say for sure.
But, the subclass specific attributes could be foo_id and foo_type
which would form the base of the foo class.
The one rough edge is there is no wrapper around the whole thing that
will return an Item. I don''t know enough yet about the magic of Rails
and ActiveRecord to do this. It seems like it could be done somehow.
If anyone finds this useful, let me know. I''d like to hear what
others think about it.
Thank you,
Perry
--
Posted via http://www.ruby-forum.com/.
--~--~---------~--~----~------------~-------~--~----~
You received this message because you are subscribed to the Google Groups
"Ruby on Rails: Talk" group.
To post to this group, send email to
rubyonrails-talk-/JYPxA39Uh5TLH3MbocFFw@public.gmane.org
To unsubscribe from this group, send email to
rubyonrails-talk-unsubscribe-/JYPxA39Uh5TLH3MbocFFw@public.gmane.org
For more options, visit this group at
http://groups.google.com/group/rubyonrails-talk?hl=en
-~----------~----~----~----~------~----~------~--~---