The following hacks class table inheritance around the existing Rails inheritance mechanism, and seems to work pretty smoothly. It automatically creates proxy classes to hold the extra columns for each subclass, with has_one associations as appropriate. It adds eager loading for these proxy associations to your finds, and generates delegate methods for the subclass attributes.
One caveat is that it does still require you put a Rails-style ‘type’ column on the topmost base class.
I think it runs about as fast as is possible with that sort of schema… one minor irritation is that it still puts an extra condition on the ‘type’ column into a lot of the SELECTs, when it would be better if it just replaced the LEFT OUTER JOIN with a INNER JOIN.
class ActiveRecord::Base
class << self
alias_method :has_one_without_cti, :has_one
end
def self.class_table_inheritance(options = {})
table_name = options[:subclass_table] || name.demodulize.tableize
primary_key_name = options[:subclass_foreign_key] || "#{superclass.name.demodulize.underscore}_id"
proxy_symbol = "extra_columns_for_#{name.demodulize.underscore}".to_sym
class_name = '::' + self.name
proxy_class = const_set('ExtraColumns', Class.new(ActiveRecord::Base))
proxy_class.class_eval do
set_table_name table_name
set_primary_key primary_key_name
belongs_to :base, :class_name => class_name, :foreign_key => primary_key_name
def self.reloadable?; false; end
end
has_one_without_cti proxy_symbol, :class_name => proxy_class.name, :foreign_key => primary_key_name, :dependent => true
# We need the after_save filter for this association to run /before/ any other after_save's already registered on the superclass,
# and before any after_creates or after_updates. This calls for some hackery:
proxy_save_callback = @inheritable_attributes[:after_save].pop
@inheritable_attributes[:after_create] ||= []
@inheritable_attributes[:after_create].unshift(proxy_save_callback)
@inheritable_attributes[:after_update] ||= []
@inheritable_attributes[:after_update].unshift(proxy_save_callback)
class_eval <<-EOV
def self.find(*params)
if params.last.is_a?(Hash)
opts = params.last
else
opts = {}
params.push(opts)
end
opts[:include] ||= []
opts[:include] = [opts[:include]] unless opts[:include].is_a?(Array)
opts[:include] << :#{proxy_symbol}
super(*params)
end
alias_method :#{proxy_symbol}_old, :#{proxy_symbol}
def #{proxy_symbol}
#{proxy_symbol}_old or self.#{proxy_symbol} = ExtraColumns.new
end
def save
self.#{proxy_symbol} ||= ExtraColumns.new
super
end
# this doesn't happen automatically on update, so we'll make it:
after_update {|record| record.#{proxy_symbol}.save }
# associations on this subclass get added to the proxy class, and then the relevant methods delegated to the proxy object
def self.belongs_to(name, *params)
ExtraColumns.belongs_to(name, *params)
delegate name, "\#{name}=".to_sym, "\#{name}?".to_sym, "build_\#{name}".to_sym, "create_\#{name}".to_sym, :to => :#{proxy_symbol}
end
def self.has_one(name, *params)
ExtraColumns.has_one(name, *params)
delegate name, "\#{name}=".to_sym, "build_\#{name}".to_sym, "create_\#{name}".to_sym, :to => :#{proxy_symbol}
end
def self.has_many(name, *params)
ExtraColumns.has_many(name, *params)
delegate name, "\#{name}=".to_sym, "\#{name.to_s.singularize}_ids=".to_sym, :to => :#{proxy_symbol}
end
def self.has_and_belongs_to_many(name, *params)
ExtraColumns.has_and_belongs_to_many(name, *params)
delegate name, "\#{name}=".to_sym, "\#{name.to_s.singularize}_ids=".to_sym, :to => :#{proxy_symbol}
end
EOV
delegate_methods = proxy_class.column_names + proxy_class.column_names.map {|name| "#{name}=".to_sym } + proxy_class.column_names.map {|name| "#{name}?".to_sym }
delegate *(delegate_methods << {:to => proxy_symbol})
end
end
Quick example of use:
create table animals (id int not null primary key, type varchar, name varchar);
create table pigs (animal_id int not null primary key references animals (id), piggyness int);
create table muddy_pigs (pig_id int not null primary key references pigs (id), muddyness int);
# note; subclass tables should not have an 'id'
# column, rather their primary key should be
# the foreign key column pointing to the
# superclass row's primary key
class Animal < ActiveRecord::Base
end
class Pig < Animal
class_table_inheritance
end
class MuddyPig < Pig
class_table_inheritance
end
MuddyPig.create(:name => 'betty', :piggyness => 3, :muddyness => 5)
s = MuddyPig.find(:first)
=> #<MuddyPig:0x12345678 @attributes={"name" => "betty"} @_muddy_pig_proxy=#<MuddyPig::Proxy @attributes={"piggyness" => 3}> @_pig_proxy=#<Pig::Proxy @attributes={"piggyness" => 3}>
s.piggyness = 567
s.save
# polymorphic find doesn't eager load the associations, since results may not all be of the same class,
# but does instantiate the right class if you include a 'type' column
Animal.find(:all)
=> [#<MuddyPig:0x12345678 @attributes={"name" => "betty", "type" => "MuddyPig"}>]
# Another query example - note that you need to qualify your column names.
# Because it adds eager loading you can use columns from all the parent tables
# in your query too (in MySQL this works at least)
Pig.find(:all, :conditions => "pigs.piggyness > 1 and animals.name like 'b%'")
# note that things like Pig.find_by_piggyness don't currently work, as piggyness isn't a column of the base table
Anyone interested in using this code or making into a plugin feel free. matt at state51 dot com
Or you can download it already as a plugin
While using this code I came across two problems:
- my subclass includes a habtm association. When adding a record to this collection, the foreign key in the DB does not get filled. However, no error is thrown - also I get an exception when dividing my model into modules. Calling find() will result in the message ‘ModuleName is not missing constant ClassName’ (with the appropriate names for class and module, of course)Of a few issues with this. It is a quick hack rather than something that’s fully tested for compatibility with all of ActiveRecord.
I’m afraid any further effort on my part is likely to go into moving to DataMapper ( http://datamapper.org/ ) and working on CTI support for DataMapper.
-Matt
is located here: http://github.com/kschiess/armit/tree/master
- I played with this (armit) for about 2 hours and couldn’t get it to work at all. There is no documentation, no indication of what version of rails it is for, and so far no response from the developer to email. If anyone has gotten this to work, please post an example.