Single Table Inheritance (STI)
An Active Record Association that associates instances without a foreign key.
Prerequisite:
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. 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 to a single table. By using STI, we follow the 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
, which let Rails know that we are using an 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")
Rails automatically sets 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.