10 Dec 2009
Because single table inheritance (STI) has been invaluable to me in some recent Rails projects, I thought I’d try to dispel a little of its negative reputation by writing about some reasons you might use it, when to avoid it, and some tips for working with it. If you gave up on Rails’s STI a while ago because it required too many workarounds, I’d recommend giving it another look, as some annoyances (eg, regarding validation and demodulization) have recently been fixed.
For this article I’m assuming you know what STI is and how to set it up (add a type column and inherit from a parent class). I’m also assuming that you’re familiar with polymorphic associations and using modules to share code among different classes. Just because you know how to use these techniques doesn’t mean deciding which one to use is easy.
Suppose you have three classes in your application which model similar things. To make this easier to think about I’ll be referring to some hypothetical classes by name: Car, Truck, and Motorcycle. Let’s consider three choices for modeling this situation:
With Polymorphic Associations we use modules to share code among classes. A Single Class is not exactly a design pattern, or anything particularly interesting. I’m thinking of a model with a type-like attribute (maybe called kind) and some if statements in methods where you need different behavior for different kinds of objects. OOP purists hate this, but it works well in the real world when there are only a few slight differences between object types and separate classes are overkill.
When deciding how to design your data models, here are some questions to ask yourself:
First and foremost, your design choice needs to be understandable. In the case of a car, a truck, and a motorcycle, it seems perfectly reasonable to think of them all as vehicles. If you add a bicycle and a wheelbarrow it may become confusing because, in our minds, these things are not as vehicle-like as a car. This is to say: don’t use single table inheritance just because your classes share some attributes (eg: num_wheels, color, length), make sure there is actually an OO inheritance relationship between each of them and an understandable parent class. And be sure to choose class names carefully.
If you want to list the objects together or run aggregate queries on all of the data, you’ll probably want everything in the same database table for speed and simplicity, especially if there is a lot of data. This points to a Single Table Inheritance or a Single Class design.
Remember that while SQL is optimized for doing joins and ActiveRecord provides tools to make them easier, data separation may not be worth the increase in complexity for database operations. In the real world, 100% normalized data is not always the best design.
How many database columns are shared by every model? If there are going to be many model-specific columns, you should consider Polymorphic Associations. On the other hand, if a Car, Truck, and Motorcycle all have the same attributes, eg:
colorengine_sizepricebut different method implementations, eg:
drivetrain_weight # sums different componentsvalue_after_depreciation(years) # uses different depreciation ratesdrivers_license_certifications # references different certificationsthen Single Table Inheritance is probably a good design choice. If there are only minor differences in a few methods, you may want to “cheat” and go with a Single Class.
Here are some things that can make your STI experience more enjoyable.
This may not always apply, but I have yet to see a case where STI works well with multiple controllers. If we are using STI, our objects share a set of IDs and attributes, and therefore should all be accessed in basically the same way (find by some attribute, sort by some attribute, restrict to administrators, etc). If presentation varies greatly we may want to render different model-specific views from our controller. But if object access varies so much that it suggests separate controllers, then STI may not have been the correct design choice.
If you’re using Rails’s fixtures for testing your app, remember that there is a one-to-one relationship between fixture files and database tables. With STI, everything is in one table, so in our example we’d have a single test/fixtures/vehicles.yml file with all our fixtures in it (be sure to provide a type attribute value for every record).
If you’ve ever tried to add STI to an existing Rails application you probably know that many of your link_to and form_for methods throw errors when you add a parent class. This is because ActionPack looks at the class of an object to determine its path and URL, and you haven’t mapped routes for your new subclasses. You can add the routes like so, though I don’t recommend it:
# not recommended:
map.resources :cars, :as => :vehicles, :controller => :vehicles
map.resources :trucks, :as => :vehicles, :controller => :vehicles
map.resources :motorcycles, :as => :vehicles, :controller => :vehicles
This only alleviates a particular symptom. If we use form_for, our form fields will still not have the names we expect (eg: params[:car][:color] instead of params[:vehicle][:color]). Instead, we should attack the root of the problem by implementing the model_name method in our parent class. I haven’t seen any documentation for this technique, so this is very unofficial, but it makes sense and it works perfectly for me with Rails 2.3 and 3. For example:
def self.model_name
name = "vehicle"
name.instance_eval do
def plural; pluralize; end
def singular; singularize; end
def human; singularize; end # only for Rails 3
end
return name
end
That probably looks confusing, so let me explain:
ActionPack calls model_name on the class of the object passed to a URL-generating method, (eg: link_to("car", car)). It expects the return value to implement the plural and singular methods, so we add them to the "vehicle" string object using instance_eval. The model_name method is then inherited by the Car, Truck, and Motorcycle subclasses, and ActionPack will see all of these objects as belonging to the parent class (in this case Vehicle), and thus use the parent class’s named routes (VehiclesController). This is all assuming you’re using Rails resource-style (RESTful) URLs. (If you’re not, please do.)
To investigate the model_name invocation yourself, see the Rails source code for the ActionController::RecordIdentifier#model_name_from_record_or_class method.
Let’s say we want to provide a single form for creating a Vehicle object, so the first field is a select menu where the user chooses the object class (Car, Truck, or Motorcycle). How do we know what the options are without hard-coding the class names into the select field? We can use Ruby’s inherited hook method to keep a running list of children as they inherit from the parent. It turns out that ActiveRecord actually does this already, so we could implement a Vehicle.select_options method like this:
def self.select_options
subclasses.map{ |c| c.to_s }.sort
end
Internally, ActiveRecord uses a class variable @@subclasses which is accessible only through the protected method subclasses. This is another trick I discovered while reading the Rails source code (“unofficial” as far as I know), so if you don’t like the feel of it we can re-implement the behavior directly in our parent class:
@child_classes = []
def self.inherited(child)
@child_classes << child
super # important!
end
def self.child_classes
@child_classes
end
In either case, we still have one more problem to solve: child classes are not recognized by the parent until they are loaded. In typical Rails production and test environments this happens right away, but in development, where config.cache_classes == false, classes aren’t loaded until you call upon them. So, for this to work consistently in our development environment we need to manually require classes:
# add to config/environments/development.rb:
%w[vehicle car truck motorcycle].each do |c|
require_dependency File.join("app","models","#{c}.rb")
end
It’s important to understand that classes are lazy loaded in the development environment so you avoid serious problems.
That’s all for now. Good luck.