Single Table Inheritance (STI)

An Active Record Association that associates instances without a foreign key.

Prerequisite:

Table of Contents

The Foundation

The Example

Create Table

Create Types

Data
Extracting Data

The Foundation

All the 6 basic associations that I covered in Part 1, Part 2, Polymorphic & Self-Join had one thing in common, all of them had at least 1 foreign key in any one table. However, foreign-key isn’t needed to implement STI because as the name suggests, Single Table Inheritance, requires Single Table.

A single table without a foreign key? How would we associate with anything without a foreign key? A better question, what is the purpose of an STI in the first place?

To answer the question let's discuss an example. Let's say we have a table A that has x number of attributes in it. And we wanted to create another table B with the same-to-same attributes as the table A. And then we wanted to create another table with the same attributes and so on… You see where this is going. The only difference between these tables is its name, the rest of it such as its attributes’ name and its type are the same for all tables. The concept of repeating ourselves with the same code is not what Ruby is about. STI helps us to avoid the creation of multiple tables with similar attributes with a single table. By using STI, we follow Do-not Repeat Yourself (DRY) concept.

STI is a Rails technique to categorize multiple types in a single table by having multiple types model classes that inherit from single tables’ model class.

An Example

Let's understand the implementation of STI with an example.

“A list of entertainment shows, such as a movie, TV-series or a documentary.”

Movies, TV series, and documentaries have a lot of common features such as name, runtime, release_date, language etc. So instead of creating 3 different tables of these, we create a single table that holds records of all.

But how would we know whether a particular record in a table is of a movie, TV series, or documentary? We explicitly create an attribute in the table named ‘type’ where each record can either have a value of as a movie, TV series, or documentary.

It is this STI attribute, type, that lets rails know that we are using STI association. It's a convention provided by Rails that we follow in order to enable a certain feature.

Note, a foreign-key name followed by _type is a polymorphic attribute used for polymorphic association.

Create Table

Now, let's create a table with the example discussed above. Since the table will hold the values of different types, we would appropriately name the table that categorizes our types.

rails g model Show name:string streaming_at:string released_on:date type:string

As discussed above, to implement STI we create type attribute.

Migration file:

The Show Model class file:

class Show < ApplicationRecord
end

Create Types

A shows table is a representation of all 3 types in our example, hence, we create only their model files without a table for each.

rails g model Movie --parent=Showrails g model TvSeries --parent=Showrails g model Documentary --parent=Show

The — parent option let rails know that we want to create model files without a table & a migration file and inherit model class directly from the Show class instead of the ApplicationRecord class. This means that all behavior added to Show class is available for Movie, TvSeries & Documentary model class too, such as associations, public methods, validations, etc.

A peek into the model files generated :

# app/model/tv_series.rb
class TvSeries < Show
end
# app/model/movie.rb
class Movie < Show
end
# app/model/documentary.rb
class Documentary < Show
end

Data

Now to save records in the Show table we would save it the same way as if we had 3 different tables for each type as:

# movie = Movie.create(name: "Moonlight", streaming_at: "Amazon Prime", released_on: "18/11/2016")# tv = TvSeries.create(name: "Tiny World", streaming_at: "Apple TV+", released_on: "02/10/2020")# doc = Documentary.create(name: "Life in Colour", streaming_at: "Netflix", released_on: "22/04/2021")
The data we created in 'shows' table.

Rails automatically set the value of the type for each record by the name of the model class we initiated while creating a record.

Extracting data

$ Movie.all
=> #<ActiveRecord::Relation [#<Movie id: 1, name: "Moonlight", streaming_at: "Amazon Prime", released_on: "2016-11-18", type: "Movie", created_at: "##", updated_at: "##">]
$ Documentary.all
=> #<ActiveRecord::Relation [#<Documentary id: 3, name: "Life in Colour", streaming_at: "Netflix", released_on: nil, type: "Documentary", created_at: "##", updated_at: "##">]
$ Show.all
=> #<ActiveRecord::Relation [#<Movie id: 1, name: "Moonlight", streaming_at: "Amazon Prime", released_on: "2016-11-18", type: "Movie", created_at: ##, updated_at: ##>, #<TvSeries id: 2, name: "Tiny World", streaming_at: "Apple TV+", released_on: "2020-10-02", type: "TvSeries", created_at: ##, updated_at: ##>, #<Documentary id: 3, name: "Life in Colour", streaming_at: "Netflix", released_on: "2021-04-22", type: "Documentary", created_at: ##, updated_at: ##>]

I hope I have achieved to make you understand the topic thoroughly. If you have any questions or other comments, feel free to leave them below. Also, if you found this article useful, you can share it so others can find it as well.