Single-table inheritance was a difficult concept for me to grasp when I first started using Ruby on Rails. Part of this might stem from the fact that it’s not a perfectly modeled concept in Rails itself. Relational databases like MySQL, PostgreSQL, and sqlite don’t have any concept of (inherited) sub-tables.
Let’s say you want to track pets. Cats and dogs need to be treated differently, but they basically have the same set of attributes, and share some methods as well. Here’s how we setup our models:
# app/models/pet.rb class Pet < ActiveRecord::Base validates_attributes :name def speak '' end end
# app/models/dog.rb class Dog < Pet def speak 'Woof!' end end
# app/models/cat.rb class Cat < Pet def speak 'Meow!' end end
Notice we include a default
speak method in the parent class. This isn’t required, but it will help with any other pet types we may create in the future. This will be the default, so if we add a Fish category, we don’t need to define its own
speak method separately.
All three models will share a common database table. In our migration, we’ll need to add a string column called “type”, that will contain the class name of the individual record:
class CreatePets < ActiveRecord::Migration def self.up create_table :pets do |t| t.string :name t.string :type t.timestamps end end def self.down drop_table :pets end end
This is how Rails will know which records are cats, and which are dogs. You’ll usually never have to work with this column directly. Now calls to
Dog.all will return only dog records from the pets table. Calling
speak on a pet record will return “Woof!” for a dog record, and “Meow!” for a cat record.
Now, how do we test this? We should create a test file for each model class. We’ll test everything in that is included in the parent model in its test file, then only test the things that change in the child model tests:
# test/unit/pet_test.rb require 'test_helper' class PetTest < ActiveSupport::TestCase def test_should_validate_presence_of_name pet = Pet.new() assert !pet.valid? assert pet.errors.on(:name).include?("can't be blank") end def test_should_speak_blank pet = Pet.new assert_equal '', pet.speak end end
class DogTest < ActiveSupport::TestCase def test_should_bark dog = Dog.new assert_equal 'Woof!', dog.speak end end
class CatTest < ActiveSupport::TestCase def test_should_meow cat = Cat.new assert_equal 'Meow!', cat.speak end end
Single-table inheritance is powerful, and only slightly tricky to grasp. It’s well worth learning.