Rails STI: Single-Table Inheritance vs. Polymorphism

Photo of Katarzyna Pałyz

Katarzyna Pałyz

Updated Nov 29, 2024 • 9 min read

When designing relations between models in a Rails app, it can sometimes occur that there is a need to have several models related to another model. How do we deal with such a situation?

Rails provides two ways to handle it – Single Table Inheritance and Polymorphic Association.

Single Table Inheritance

Sometimes we have multiple models which have different behavior and different methods defined but their same attributes are shared. This is a situation where we can use Single Table Inheritance.

Single Table Inheritance (STI) models are defined as separate classes inheriting from one base class, but they aren’t associated with separate tables — they share a single database table. The table contains a type column that defines which subclass an object belongs to.

Let's say we have three simple model classes: Post, Student, and Teacher. A Post can belong to a Student or to a Teacher. A Student and a Teacher are quite similar: all the attributes they have are just first_name and last_name. There is only a difference in behavior between them. Adding a Post requires validation if it is done by a Student, and the validation is not needed for a Post added by a Teacher. Also, we want to send a notification if a Teacher adds a Post.

Post
can belong to a Student
can belong to a Teacher

Student
first_name, last_name
can have many Posts
adding a Post:
- validate content
- create a Post

Teacher
first_name, last_name
can have many Posts
adding a Post:
- create a Post
- send notification

To implement STI in this case, we need to create a base model for the classes with shared attributes (Student and Teacher) and a relation to the Post class. For example, we can define Student class and a Teacher class as subclasses of a User superclass. The relation to Post can be defined in the User class, and the differences in behavior are reflected by defining different methods in the subclasses.

class User < ApplicationRecord
has_many :posts
end

class Student < User
def add_post
validate_content
create_post
end
end

class Teacher < User
def add_post
create_post
send_notification
end
end

And we define the relation to User in the Post model.

class Post < ApplicationRecord
belongs_to :user
end

In such a case, both Student and Teacher models data are stored in one table: users. And the relation with Post is reflected by the user_id column in the posts table.

create_table "users", force: :cascade do |t|
t.string "type", null: false
t.string "first_name"
t.string "last_name"
...
end

create_table "posts", force: :cascade do |t|
t.bigint "user_id", null: false
...
end

The users table has two model data fields — first_name and last_name, and a type field which can be a “Student” or a “Teacher” string and cannot contain null values. Rails uses type as a default column name, but it can be overridden in the User class code:

class User < ApplicationRecord
self.inheritance_column = 'kind_of_user'
end

Creating Student and Teacher objects can be done directly by using a subclass:

Student.create(first_name: 'John', last_name: 'Doe')

or by using a User superclass:

User.create(type: 'Student', first_name: 'John', last_name: 'Doe')

but then we need to remember about passing a valid type (non-existing class names will result in an ActiveRecord::SubclassNotFound error).

STI seems to be a neat, dry solution for handling such a case. However, it can become problematic if our models evolve and we add some custom attributes to them, or if we add more and more models and our single table grows.

Single Table Inheritance

Single Table Inheritance (STI) is a design pattern in Ruby on Rails that allows multiple models to share the same database table. This approach eliminates the need for separate tables for each model, providing a more streamlined and efficient database structure. STI is particularly useful when modeling different types of objects that share common attributes while maintaining their distinct characteristics. The type column is used to distinguish between different types of objects in a single table, ensuring that each record is correctly identified and managed.

For instance, consider a scenario where you have different types of users, such as Student and Teacher, both sharing common attributes like first_name and last_name. Instead of creating separate tables for each model, you can use STI to store all user data in a single users table. The type column in this table will indicate whether a record is a Student or a Teacher, allowing Rails to handle the data appropriately.

Polymorphic Associations

Sometimes we have some completely different models, with different attributes and different behaviors, and the only thing they have in common is the relation to a third model. It is a good case for using a Polymorphic Association.

In Polymorphic Association, one model can belong to several other models, using a single association. The relation is defined in the “polymorphic” table by having two columns that hold the classname and the id of the related object.

Having the example of Post, Student, and Teacher models, let's say we need to track more attributes of a Student — we would like to have their student_id_number, faculty, and master_thesis_topic. For a Teacher, we would like to store the employee_card_number, department, and years_of_experience. Now our models are completely different — they have different attributes and behavior. Their only common part is their relation to the Post model.

Post
can belong to a Student
can belong to a Teacher

Student
student_id_number, faculty, master_thesis_topic
can have many Posts
adding a Post:
- validate content
- create a Post

Teacher
employee_card_number, department, years_of_experience
can have many Posts
adding a Post:
- create a Post
- send notification

To use Polymorphic Association here, we need to define the polymorphic association in our models, using the Rails convention of naming the relation with the '-able' suffix:

class Post < ApplicationRecord
belongs_to :postable, polymorphic: true
end

class Student < ApplicationRecord
has_many :posts, as: :postable

def add_post
validate_content
create_post
end
end

class Teacher < ApplicationRecord
has_many :posts, as: :postable

def add_post
create_post
send_notification
end
end

In the posts table we need to have postable_type and postable_id columns which determine the classname (Student or Teacher) and the id of the associated object.

create_table "posts", force: :cascade do |t|
t.string "postable_type", null: false
t.bigint "postable_id", null: false
...
end

Post objects can be created by passing postable directly as an object:

post = Post.create(postable: Student.first)

or by passing postable_type and postable_id:

post = Post.create(postable_type: 'Student', postable_id: 1)

Rails provides a validation of the presence of the postable object when creating a Post, however, there is no validation at the database level — we cannot add a foreign key. Therefore, with direct access to the database, we can use an sql query which will create posts with non-existing objects associated — which sometimes can be a serious issue.

ActiveRecord::Base.connection.execute(
"INSERT INTO posts (postable_type, postable_id) VALUES ('Student', 9999)"
)
# creates a record even if there is no Student with id=9999

Post.last.postable
# nil

Summary

Both of these relationship models can be very useful. But also both of them have some drawbacks.

Single Table Inheritance:

  • Suitable for a set of models with shared attributes.
  • Models can have different behavior.
  • May be poorly scalable if the number of models grows or custom fields are added to them.

Polymorphic Associations:

  • Suitable for several models with different attributes and behavior, but all related to one model.
  • Easy to scale.
  • No support for foreign key — can be dangerous with the possibility of having non-existing records associated.

The decision of which strategy to choose should be made based on the individual requirements, having analyzed all the pros and cons, and also having in mind the future evolution of the application.

Photo of Katarzyna Pałyz

More posts by this author

Katarzyna Pałyz

Read more on our Blog

Check out the knowledge base collected and distilled by experienced professionals.

We're Netguru

At Netguru we specialize in designing, building, shipping and scaling beautiful, usable products with blazing-fast efficiency.

Let's talk business