Two comments:
1. Instead of using CLSTI, another solution is to create an updatable
view for work_products_and_features which will be more efficient since
it will be only one database call for the insert. (I am not sure about
mysql but I believe postgresql supports updatable view for more than one
table),
2. If using CLSTI, how do we handle transaction between the two save?
Since the parent save happens right before child save, what will happen
if the child save fails?
Thanks
chong
-----Original Message-----
From: rails-bounces-1W37MKcQCpIf0INCOvqR/iCwEArCW2h5@public.gmane.org
[mailto:rails-bounces-1W37MKcQCpIf0INCOvqR/iCwEArCW2h5@public.gmane.org] On
Behalf Of John Wilger
Sent: Tuesday, January 04, 2005 9:27 AM
To: rails-1W37MKcQCpIf0INCOvqR/iCwEArCW2h5@public.gmane.org
Subject: [Rails] Class Table Inheritance for ActiveRecord
Why yes, I _have_ still been working on this. ;-)
With the changes DHH made a while ago that cause STI to only kick in if
the ''type'' column is defined, It seemed like it might be
overkill to add
a lot of new grammar to ActiveRecord::Base to support the different
inheritance models. Taking a cue from ActiveRecord::Acts, I started
working out how to use an included module to implement CLSTI (Class
Table Inheritance). I''ve in-lined the beginnings of such a module
below.
It''s far from complete, but I wanted to get some feedback as to whether
people think this is on the right track.
As far as usage is concerned, lets say you have the following MySQL
table definition:
create table status (
id integer unsigned not null auto_increment PRIMARY KEY,
name varchar(50) not null,
UNIQUE INDEX status_name_uq(name)
) ENGINE=INNODB;
create table work_products (
id integer unsigned not null auto_increment PRIMARY KEY,
name varchar(100) not null,
status_id integer unsigned not null,
description text,
CONSTRAINT work_products_status_id_fk
FOREIGN KEY work_products_status_id_fk(status_id)
REFERENCES status(id)
ON DELETE RESTRICT
ON UPDATE CASCADE
) ENGINE=INNODB;
create table features (
id integer unsigned not null PRIMARY KEY,
release varchar(50) not null,
CONSTRAINT features_id_fk
FOREIGN KEY features_id_fk(id)
REFERENCES work_products(id)
ON DELETE CASCADE
ON UPDATE CASCADE
) ENGINE=INNODB;
We would then create the following model classes:
class Status < ActiveRecord::Base
has_many :work_products
end
class WorkProduct < ActiveRecord::Base
belongs_to :status
end
require ''class_table_inheritance''
class Feature < WorkProduct
include ClassTableInheritance
end
The inclusion of the ClassTableInheritance module in Feature will allow
us to access the database columns defined for WorkProduct directly while
still saving them as two separate objects/database rows:
feature = Feature.new
feature.name = "Some Feature"
feature.status = Status.find(''1'')
feature.description = "This is just a feature."
feature.release = "Version 1.0"
feature.save
same_feature = Feature.find(feature.id)
same_feature.name == feature.name #-> true
#etc.
As I said, this module is far from complete, and many issues are left
unaddressed at this point. I just want to get some early feedback to
make sure no one spots any big problems that would render further work
on this path useless.
-- SNIP: lib/class_table_inheritance.rb --
# ClassTableInheritance is a mix-in module for ActiveRecord model
classes # which implements the class table inheritance design pattern.
module ClassTableInheritance
# Redefine #method_missing to allow content columns from the
# superclass to be accessed directly but still send the messages to
# the associated class. I''m worried that using #send could be
somewhat
# dangerous here, because it bypasses the public/protected/private
# status of the method.
def method_missing(symbol, *args)
begin
super(symbol, *args)
rescue NoMethodError
self._cti_parent.send(symbol, *args)
end
end
# A class including the ClassTableInheritance module _must_ have an
# existing association with an instance of its superclass. The actual
# association is declared in ClassTableInheritance.included
def after_initialize
if new_record?
self._cti_parent = self.class.superclass.new
end
end
# This may need to be reworked. Does ActiveRecord always refer to the
# primary_key field using the +id+ attribute?
def before_save
self._cti_parent.save
self.id = self._cti_parent.id
end
module ClassMethods
# ActiveRecord uses the class name of the first descendant from
# ActiveRecord::Base by default when determining the table name, so
# we override this behavior here.
def table_name
table_name_prefix + undecorated_table_name(self.name) +
table_name_suffix
end
# Methods added via the association class methods will be inherited
# by subclasses, but we want them to be handled by the associated
# instance of the superclass.
# Executed via ClassTableInheritance.include
def remove_superclass_association_methods
associations = superclass.reflect_on_all_associations
unless associations.nil?
associations.each do |assoc|
undef_method assoc.name
end
end
end
# Methods added via the aggregation class methods will be inherited
# by subclasses, but we want them to be handled by the associated
# instance of the superclass.
# Executed via ClassTableInheritance.include
def remove_superclass_aggregation_methods
aggregations = superclass.reflect_on_all_aggregations
unless aggregations.nil?
aggregations.each do |aggr|
undef_method aggr.name
end
end
end
end
# When this module is included, set up a +belongs_to+ relationship
# with the including class''s superclass under the symbol
:_cti_parent.
def self.included(klass)
klass.extend(ClassMethods)
klass.class_eval <<-EOF, __FILE__, __LINE__
belongs_to(:_cti_parent,
:class_name => self.superclass.name,
:foreign_key => self.primary_key)
remove_superclass_association_methods
remove_superclass_aggregation_methods
EOF
end
end
-- END SNIP --
_______________________________________________
Rails mailing list
Rails-1W37MKcQCpIf0INCOvqR/iCwEArCW2h5@public.gmane.org
http://lists.rubyonrails.org/mailman/listinfo/rails