[OLD] Class-table inheritance in ActiveRecord
January 19th, 2008
This article is nothing new. I first published it on November 7th, 2006. The site it went up on is no longer running, but it seemed a shame to let it disappear. I imagine the ActiveRecord codebase has moved on significantly enough that the patch would be very difficult to apply now.
There were quite a few comments to the original post (not preserved here) expressing interest in CTI. Unfortunately, there were none from any of the ActiveRecord team, but then, DHH doesn't even like foreign keys, so expecting interest in CTI would be downright naive.
The original article
I'm pretty fanatical about making sure databases are correct and unambiguous. Unfortunately, single-table inheritance makes this hard, as you have to store attributes about distinct types of entities together in one table. To me, class-table inheritance is the correct way to record information when you have an entity type hierarchy in your database (and therefore a class hierarchy in your application).
About the implementation
Normalised data
I started by solving the relatively simple problem of reading and writing attributes in satellite tables. This is useful in itself for various reasons I won't bore you with now, but it's an essential part of the CTI implementation.
There's a new normalized_data_test.rb file in the tests that shows how this works. It consists of two class level methods in ActiveRecord::Base, normalized_attribute and normalized_attribute_group. Columns of the satellite table appear to your application to be in the model table itself.
Reading the class-tables
I've tried to make CTI work as transparently as STI. Given these tables...
CREATE TABLE people (
id serial PRIMARY KEY,
first_name varchar,
last_name varchar
);
CREATE TABLE employees (
person_id integer NOT NULL REFERENCES people (id),
salary decimal(10,2),
hired_at timestamp
);
and these classes...
1 2 3 |
class Person < ActiveRecord::Base; end class Employee < Person; end |
then ActiveRecord will follow this procedure (or something evquivalent, as the algorithm has to rerun when classes are reconfigured) to initialise inheritance:
- check the people table exists
- check for a type column in people
- if it does - use STI and stop here
- if not - assume CTI
- check the employees table exists
The convention is that the primary key in each table is named after the immediate supertype. To add a Manager subtype to the above example:
CREATE TABLE managers (
employee_id integer NOT NULL REFERENCES employees (employee_id),
has_pointy_hair boolean
);
1 2 |
class Manager < Employee; end |
Manual overrides
If you are working with a legacy schema, you can override the table name and the primary key (which is actually a foreign key but I wanted to re-use the method name).
1 2 3 4 5 |
class Customer < ActiveRecord::Base set_table_name "suckers" set_primary_key "sucker_id" end |
Mixing inheritance types
I've tried to make CTI integrate as seamlessly as possibly with STI. You can use both in the same inheritance chain, as long as STI is at the end. For example, to extend the example above:
ALTER TABLE managers ADD COLUMN type varchar;
You can then do:
1 2 |
class ImportantManager < Manager; end |
There are three reasons I didn't attempt to make STI work in the middle of an inheritance chain:
- it would massively complicate the algorithm that determines the class type
- it would mean maintaining the inheritance column ("type") to store names of classes that actually have their own table, potentially an integrity nightmare (not to mention the fact you could potentially have multiple blocks of STI in an inheritance chain)
- I don't imagine anyone would actually want to do it (if you want to switch from STI to CTI, refactor your database!)
What's left to do
There are a few minor things, mainly:
- Currently CTI objects let you call attribute accessors that should only exist in subclasses, but I'll fix that soon.
- I need to test that the mixed inheritance types work as described above.
I've tested that CTI works on its own. I've tested that everything (well, except for one thing) that used to work in ActiveRecord still works with CTI in place, just not using it. However... I haven't tested that all the existing ActiveRecord features work with CTI classes. The reason is that this could potentially involve duplicating most of the existing unit tests, and I want to get some feedback from the Rails community first. I imagine the associations still need some major work.
The patch is available here: [http://dev.rubyonrails.org/ticket/6566]
Feel free to leave feedback
Links
I've had a search on the interweb and these are the most interesting articles about CTI in Rails:

Leave a Reply