Rails STI: Single-Table Inheritance vs. Polymorphism
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.