Topic: Definitive Guide to Single Table Inheritance

After trying to implement this in my project I am realizing just how scant and misleading the docs are on this topic. This thread is more of a request than a tutorial and I am hoping that some of you more experienced Rails folk will fill out the missing pieces of the puzzle.

To make your jobs easier, I will cover what I do know...

When to use STI:
This structure is best used for models that have identical, or very similar attributes. A perfect example would be a table called cars. You can have columns like horsepower, gas_mileage, price etc. since all cars are basically categorized in the same fashion, STI is a perfect fit.

On the other hand, if your subclasses are pretty different from each other, then they deserve their own table.

The database:
Rails supports STI out of the box. All you need to do is create a table with a column called "type". You can override this convention and use a differnt name for the column, but for simplicity's sake we wont go there.

Here is the migration file.

[code="Ruby]
class CreateCars < ActiveRecord::Migration
  def self.up
    create_table :cars do |t|
      t.column :type, :string
      t.column :name, :string
      t.column :price, :string
      t.column :gas_mileage, :string
      t.column :horse_power, :string
    end
  end

  def self.down
    drop_table :cars
  end
end   
[/code]

Once the table has been created, it's time to make our model file, car.rb:

class Car < ActiveRecord::Base
end

class Ford < Car
end

class Toyota < Car
end


With our Model ready to go its time to set up CarController.rb.

And this is where I get completely lost. Niether the Rails API or the AWD book offer much help in this department, and the Wiki's code is outdated ( Tons of @params and no RESTful routes) Also, this bit of view code:

<%= hidden_field_tag "option_type", @option[:type] %>

Is completely worthless and throws up an error. Some people have commented this on the wiki but no one has responded.

Of course I could just throw my hands up and use multiple tables, but I know this is the proper way to do it so I hope someone will be able to pick up the baton and run with this tutorial.

THE KEY QUESTIONS:

1. How do we specify the subclass through a form? Should it be inside the form_for statement? A hidden input?
2. How do we set up the find and create methods for subclasses in the model and then reference them in the controller?
3. How do we write the index and delete actions?

Re: Definitive Guide to Single Table Inheritance

Update: The find method was simpler than I thought:

@fords = Ford.find(:all)

Rails makes me slap myself sometimes wink

Still trying to figure out how to set up the controller.. researching...

Re: Definitive Guide to Single Table Inheritance

Still poking around and figured out that Rails wants all my subclasses to have their own Model.rb files. Is there a way to override this so subclasses can sit in the Parent.rb file?

Re: Definitive Guide to Single Table Inheritance

pimpmaster wrote:

Still poking around and figured out that Rails wants all my subclasses to have their own Model.rb files. Is there a way to override this so subclasses can sit in the Parent.rb file?

I don't know of the exact answer, but Rails is the first language I've worked with such that if it doesn't let you do something then it means that you probably aren't supposed to do it.

From past experience with other languages you probably want your subclasses seperated, it makes maintenance a whole lot easier.

Re: Definitive Guide to Single Table Inheritance

You may have a point there jbartels. I have taken your advice and right now I am really struggling with the create methods.

Deviating from the example above, I have Options as parent class and inside I have product and size classes.

# models/product.rb
class Product < Option
  def self.showall
    @products = Product.find(:all)
  end
end

# models/size.rb
class Size < Option
  def self.showall
    @sizes = Size.find(:all)
  end
end

# controllers/OptionsController.rb
require_dependency 'Option'

def new
    case params[:option_type]
      when "Product"
        @option = Product.new
      when "Size"
        @option = Size.new
    end
  end
 
  def create
    case @params[:option_type]
      when "Product"
        @option = Product.new(params[:option])
      when "Size"
        @option = Size.new(params[:option])
    end
    if @option.save
      flash[:notice] = 'Option was successfully created.'
      format.html { redirect_to option_url(@option) }
    else
      format.html { render :action => "new" }
    end
  end


My STI table was set up as follows:

class CreateOptions < ActiveRecord::Migration
  def self.up
    create_table :options do |t|
      t.column :type, :string
      t.column :name, :string
    end
  end

  def self.down
    drop_table :options
  end
end


The resulting error when I try to create a new option:

You have a nil object when you didn't expect it!
You might have expected an instance of ActiveRecord::Base.
The error occurred while evaluating nil.save

I've followed the rules to the letter and cant get this working. Does Rails really force me to make a controller for every subclass? That would suck.

Last edited by pimpmaster (2007-03-13 16:14:53)

Re: Definitive Guide to Single Table Inheritance

It doesn't look like you're setting the @option instance variable in the create action:

def create
  @option = Option.new(params[:option])

Railscasts - Free Ruby on Rails Screencasts

Re: Definitive Guide to Single Table Inheritance

This is too funny, I actually figured out how to do this as you wrote this post.

I ended up using the much cleaner implementation inside the model.

def self.factory(type, params = nil)
    params[:type] ||= 'Option'
    class_name = params[:type]
    if defined? class_name.constantize
    class_name.constantize.new(params)
    else
    Option.new(params)
    end
  end

Then the controller gets a revamp

def new
    @option = Option.factory(params[:option_type])
  end

  def create
    @option = Option.factory(params[:option_type], params[:option])
    respond_to do |format|
      if @option.save
        flash[:notice] = 'Option was successfully created.'
        format.html { redirect_to option_url(@option) }
      else
        format.html { render :action => "new" }
      end
    end
  end


I am so grateful that the logic is sitting comfortably in the model instead of mucking up the controller with endless case conditions. I'd be lying if I said I understood every line of this stuff, but it does appear to be working. Problem solved!

For those of you who love my endless ramblings, dont fear.. I am sure I will manage to break something else and come back crying for help.

FWIW, this is the best forum I have ever frequented, Rails or not.

Cheers!

Edit: Forgot to mention that this is all working with my subclasses sitting inside my parent class model file. Yippee!

Last edited by pimpmaster (2007-03-13 16:33:18)

Re: Definitive Guide to Single Table Inheritance

jbartels wrote:
pimpmaster wrote:

Still poking around and figured out that Rails wants all my subclasses to have their own Model.rb files. Is there a way to override this so subclasses can sit in the Parent.rb file?

I don't know of the exact answer, but Rails is the first language I've worked with such that if it doesn't let you do something then it means that you probably aren't supposed to do it.

From past experience with other languages you probably want your subclasses seperated, it makes maintenance a whole lot easier.

I just read an article about this somewhere recently, but I can't find it again. I think the Car example is not an example of when you should use STI. The subclasses don't do anything different than what the parent does. You can just have a "make" column and have it hold 'ford', 'sony', 'takura'...
That's probably the reason you are trying put all the models in one file (i tried to do that too; spent a few days trying to figure out why it doesn't work); because they are small and empty.

Just wanted to mention this. I could be wrong.

edit: i think i found the article: http://blog.evanweaver.com/articles/200 … /sti-abuse

Last edited by pleatherrebel (2007-03-14 12:08:55)

Re: Definitive Guide to Single Table Inheritance

Thanks for the link dude. The concept of a make column would be perfect if it was a text input, but since I am trying to generate drop down lists to show existing makes, I figured STI was a good fit.

You are right about one thing though..my subclasses dont do anything special. I just need them to group my options into select lists and cant think of any other way.

Also, it seems redundant to have a bunch of separate tables that all have the same column (name) in them.

I definitely have to meditate on this more..

Re: Definitive Guide to Single Table Inheritance

pimpmaster wrote:

Thanks for the link dude. The concept of a make column would be perfect if it was a text input, but since I am trying to generate drop down lists to show existing makes, I figured STI was a good fit.

You can grab the a distinct list of makes from the cars table, but many times this is a sign you should extract it into its own table and model (make_id column in cars table). This way it is very easy to make a select list and you have a unique identifier for each make for easy passing, etc.

pimpmaster wrote:

Also, it seems redundant to have a bunch of separate tables that all have the same column (name) in them.

It may only require a name column at the moment, but I could see it easily expanding to include more data. For example, each Make can have its own site url which you link to, logo, etc. If the make of a car plays any significant role in the application don't feel bad about giving it its own model and table.

Railscasts - Free Ruby on Rails Screencasts

Re: Definitive Guide to Single Table Inheritance

pleatherrebel wrote:

edit: i think i found the article: http://blog.evanweaver.com/articles/200 … /sti-abuse

Great article. Thansk!

How about the case where you want to have subclasses because you want to be able to perform a different set of validations for each?  Is that kosher?

Re: Definitive Guide to Single Table Inheritance

Hi, pimpmaster, I tried to that with Rails 2.3.4 and got the below errors:

 NoMethodError in VehiclesController#new

You have a nil object when you didn't expect it!
You might have expected an instance of ActiveRecord::Base.
The error occurred while evaluating nil.[]

RAILS_ROOT: /home/serge/rails_projects/vehicles
Application Trace | Framework Trace | Full Trace

/home/serge/rails_projects/vehicles/app/models/vehicle.rb:5:in `factory'
/home/serge/rails_projects/vehicles/app/controllers/vehicles_controller.rb:14:in `new'

I tried to use your technics in my STI model: Vehicle < AR; Car < Vehicle; Moto < Vehicle:
In my rotes I defined it as follows:

map.resources :vehicles
  map.resources :cars, :controller=> 'vehicles'
  map.resources :motos, :controller=> 'vehicles'

In VehiclesController/vehicles_controller.rb:

def index    
    @vehicles = Vehicle.all
  end
  
  def show
    @vehicle = Vehicle.find(params[:id])
  end
  
  def new    
    @vehicle = Vehicle.factory(params[:type])
  end
  
  def create
    @vehicle = Vehicle.factory(params[:type], params[:vehicle])
    respond_to do |format|
      if @vehicle.save 
        flash[:notice] = 'Successfully created Vehicle!'
        format.html { redirect_to vehicles_url(@vehicle) }
      else
        format.html { render :action => "new" }
      end
    end            
  end
  
  def edit
    @vehicle = Vehicle.find(params[:id])
  end
  
  def update
    @vehicle = Vehicle.find(params[:id])
    if @vehicle.update_attributes(params[:vehicle])
      flash[:notice] = "Successfully updated vehicle."
      redirect_to @vehicle
    else
      render :action => 'edit'
    end
  end

In Vehicle model/vehicle.rb:

def self.factory(type, params = nil)
    params[:type] ||= 'vehicle'
    class_name = params[:type]
    if defined? class_name.constantize
      class_name.constantize.new(params)
    else
      Vehicle.new(params)
    end
  end

And at last in my layout application.html.erb:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
  "[url]http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd[/url]">
<html>
  <head>
    <title><%= h(yield(:title) || "Untitled") %></title>
    <%= stylesheet_link_tag 'application' %>
    <%= yield(:head) %>
  </head>
  <body>
    <div id="container">
      <ul id="menu">
        <li><%=link_to 'Cars', cars_path, :type=> 'car'%></li>
        <li><%=link_to 'Motos', motos_path, :type=>'moto'%></li>
      </ul>
      <%- flash.each do |name, msg| -%>
        <%= content_tag :div, msg, :id => "flash_#{name}" %>
      <%- end -%>

      <%- if show_title? -%>
        <h1><%=h yield(:title) %></h1>
      <%- end -%>

      <%= yield %>
    </div>
  </body>
</html>

What did I do wrong ? More of that, in my controller I had to change to lower case the line

require_dependency 'vehicle'

otherwise I had an error as Vehicle.rb fle not found. Do we really need that line? Thank you.

Re: Definitive Guide to Single Table Inheritance

Javix, I think something's gone wrong in your New controller method. Can you post that? Somewhere, a variable isn't being initialized.

I have some questions of my own. I'm trying to set up STI for some classes that will have different attributes. The superclass is Species and the subclasses are things like Bird, Tree, etc.. Bird needs attributes like wingspan, whereas Tree needs attributes like deciduous?. I know that STI isn't really recommended when you need the subclasses to have many different attributes, but it really feels like STI is the more appropriate way to do this since birds and trees truly are certain kinds of species.

Now, my species model includes each species' full taxonomy (kingdom, phylum, class, order, family, genus, species), and I could simply use the values in these attributes to identify trees or birds or mammals or whatever (e.g. birds are defined as species with the class value "Aves"). The reason that I wanted to create a species subclass is because birds and trees and other kinds of species have different attributes that I need to track. But I suppose that STI really doesn't offer any help here, since the single table would contain all of those attributes anyway. I may as well just stick with the one Species class and give it all of the attributes that I need for all of the different types of species (even though no instance of Species will ever actually need all of them), and when I'm actually dealing with an instance of Species I'll just deal with the attributes that make sense depending on the values of the taxonomy attributes? Bah, I've confused myself again. Going to go sleep on it.

I'd appreciate any guidance in thinking clearly about this.

Thanks!