Single Table Inheritance with Rails 4 – #2

Hi ! I’m finally back, sorry for the delay.

I sent my mémoire last week and I finally had some time to finish this article. Let’s continue our journey with Single Table Inheritance !

In this article, we are going to see how to create a common controller for our models (Animal, Lion, etc). We will also need to create the corresponding routes and add some helpers to generate the right paths. If you don’t have the code from the first part, you can get it here.

The common controller

Time to start working ! First, we have to generate the common controller for our models. The name is going to be AnimalsController which matches our parent model.

rails g controller AnimalsController index show new edit create update destroy

Define the routes

Let’s move to the routes file and define our routes. First, remove the lines generated by rails since we are going to use resources :

# Remove these lines from config/routes.rb
get "animals/index"
# Code hidden for brevity
get "animals/destroy"

Define the root of our app and the routes for Animals :

# config/routes.rb
Sti::Application.routes.draw do
 resources :animals
 root 'animals#index'
end

Setup the index action and the corresponding view

Fire up your server and access localhost:3000 and you will see, well nothing. We should add some content to our index view, maybe the list of all the animals in the database. To provide this list to the view, we need to add some simple code to our controller :

# app/controllers/animals_controller.rb
def index
  @animals = Animal.all
end

And copy/paste (or retype it, totally up to you) the following in animals/index.html.erb.

# app/views/animals/index.html.erb
<h1>The Lion Tribe</h1>
<table>
  <thead>
    <tr>
      <th>Name</th>
      <th>Race</th>
      <th>Age</th>
      <th></th>
      <th></th>
      <th></th>
    </tr>
</thead>
  <tbody>
    <% @animals.each do |animal| %>
      <tr>
        <td><%= animal.name %></td>
        <td><%= animal.race %></td>
        <td><%= animal.age %></td>
        <td><%= link_to 'Show', "" %></td>
        <td><%= link_to 'Edit', "" %></td>
        <td><%= link_to 'Destroy', "", method: :delete, data: { confirm: 'Are you sure?' } %></td>
      </tr>
    <% end %>
  </tbody>
</table>

The view still looks pretty bad huh, but whatever, it’s not a design tutorial. You are free to add some css (and you can even pull request on [the repo][2], I will definitely merge it if it looks good).

Now that we have a basic way to show our data, we can start playing with Single Table Inheritance. In the first part, we created one table animals and one model Animal. We also created 3 other models (Lion, Meerkat, WildBoars) that inherit from Animal and all live happily in the same table. Thanks to that, we avoided repeating code at the model level. We can follow the DRY rule at the controller level by using a unique controller for all our STI models.

Routes and STI

After a bit of code, the index action of the Animals controller will be able to show either all the animals or only the animals of a specific race. To do this, the controller need to know which race is requested. Thanks to Rails, we can define a default parameter in our routes.

# config/routes.rb
resources :animals
resources :lions, controller: 'animals', type: 'Lion' 
resources :meerkats, controller: 'animals', type: 'Meerkat' 
resources :wild_boars, controller: 'animals', type: 'WildBoar'

root 'animals#index'

See the type key ? That’s the trick. Everytime we access /lions/*, Rails will add a parameter in params with the key type. So, for each model that inherit from Animal, we define resource routes and specify that they have to use the animals controller. We also add the corresponding type.

We can now extract the type value in the controller with the following methods :

class AnimalsController < ApplicationController 
    before_action :set_race

    def index 
        @animals = Animal.all 
    end

    # Code hidden for brivety

    private

    def set_race 
       @race = race 
    end

    def race 
        Animal.races.include?(params[:type]) ? params[:type] : "Animal"
    end

    def race_class 
        race.constantize 
    end

end

Basically, before any action we assign the value of the race params, or Animal if no race is passed, to the variable @race so our views can access it. We can now change the Animal.all to race_class.all. The method race_class is going to send back the constantized parameter type like Lion for example.

def index
  @animals = race_class.all
end

Warm Up

If you access http://localhost:3000, you will have the list of all animals. But if you access http://localhost:3000/lions or http://localhost:3000/meerkats, you only get the list of the specified animal. Wonderful, isn’t it ?!

That’s it for this article. In the next part, we are going to add dynamic paths and the missing views !

Related articles

6 Comments

  1. Hi

    Excellent tutorial! It has been very useful to me.

    I have trying to fix some problems when I was following the tutorial and I end in this question at StackOverflow (link) where they point that just constantize the type coming from params is insecure (you’ll parse anything coming in type).

    So, maybe it will convenient to modify the race_class method to

    def race_classes
    ['Animal', 'Lion', 'Meerkat', 'WildBoar']
    end

    def race_class
    type.constantize if type.in? race_classes
    end

    Regards!

    David

    Reply
    • Hi David,

      That’s a very good point, I will update the code ASAP !
      It is indeed very insecure to blindly parse data coming from the client.

      Thanks for letting me know ;)

      Thibault

      Reply
  2. I keep getting this error from the index view:
    The single-table inheritance mechanism failed to locate the subclass: ‘WildBoar’. This error is raised because the column ‘race’ is reserved for storing the class in case of inheritance.

    I don’t understand, I already subclassed WildBoar to the Animal class.

    My repo can be found here: https://github.com/alvinkatojr/sti

    Reply

Leave a Comment.

To create code blocks or other preformatted text, indent by four spaces:

    This will be displayed in a monospaced font. The first four 
    spaces will be stripped off, but all other whitespace
    will be preserved.
    
    Markdown is turned off in code blocks:
     [This is not a link](http://example.com)

To create not a block, but an inline code span, use backticks:

Here is some inline `code`.

For more help see http://daringfireball.net/projects/markdown/syntax