Thursday, 23 October 2008

Single Table Inheritance

Rails supports Single Table Inheritance (STI). In this pattern, the superclass is the only model with an associated table in the database, and any object of that class and any subclass is stored in that same table. Rails use a special column named "type" which stores the class name (this means that you will have problems if you name a column "type" for any other reason).

The Models
To implement, create the top level object as a model as normal (with generate scaffold works best for me), but include a column called "type" that is a string. You also need to include every column that all your subclasses will use (though you can, of course, add columns later, as normal).

For each subclass, create a model (again, I recommend generate scaffold). Modify the model so that the class inherits from the desired superclass, and delete the database migration. Migrate the database, and you are pretty much done.

See also:

You can access all the objects in the class hierarchy through the top level class, or a specific class through other classes.

A < B < C < ActiveRecord::Base
C.find :all # Get all objects in the table, whatever the class
B.find :all # Get objects of type B only (not A)

Your superclass will need all the fields of all the subclasses - otherwise there will be no table column for field. Each subclass needs its own controller and map.resources entry in routes.rb.

Controllers and Views

So how does STI affect the controllers and the views?

Possibly not at all. You can have one controller for each model, all completely independant, all with their own set of views. However, there is a good chance that there will be some overlap. It is quite likely that you will want to use the same "show" view for each model, for example. Let us suppose SubClass is a subclass of SuperClass; the default show method is this:

def show
@sub_class = SubClass.find(params[:id])

It is very easy to point the render method to the SuperClass view (remembering to rename the variable to what super_class/show.html.erb is expecting):

def show
@super_class = SubClass.find(params[:id])
render :template => 'super_class/show'

We can go a step further. If we set up SubClassController to inherit from SuperClassController, we do not need a show method in SubClassController at all; it can all be handled in SuperClassController. This will work without any changes to SuperClassController, but your object will be of the SuperClass type, and you will lose all the benefits of subclassing. It is better to instantiate your object as the subclass:

def show
@super_class = eval("#{params[:controller].classify}.find(params[:id])")
render :template => 'super_class/show'

Other methods for the SuperClass (destroy can be used as is):
def index
@super_class = eval("#{params[:controller].classify}.find")
render :template => 'super_class/index'

def edit
@super_class = eval("#{params[:controller].classify}.find(params[:id])")
render :template => 'super_class/update'

def create
@super_class = eval("#{params[:controller].classify}.new(params[:#{params[:controller].singularize}])")
flash[:notice] = 'Record was successfully created.'
redirect_to(:controller => 'super_class' , :action => 'show')
render :template => 'super_class/new'

def update
@super_class = eval("#{params[:controller].classify}.find(params[:id])")
if @super_class.update_attributes(params[params[:controller].singularize.to_sym])
flash[:notice] = 'Record was successfully updated.'
redirect_to(:action => 'show')
render :action => 'edit'

One last point. In your helps, instead of naming a model for the links, use polymorphic, eg, edit_polymorphic_path(super_class) rather than edit_super_class_path(super_class). This will make Rails point to the right controller for your subclass, whatever it might be.

Struggling with Ruby: Contents Page


Harry said...

great summary of STI and controllers and views! Thanks!

Anonymous said...

You write:

We can go a step further.... This will work without any changes to SuperClassController, ...

I think it is not possible without any changes in SperClassController. If you put no action definition in your SubClassController how it would know that it should use view from SuperClass? I tried, but it does not work.