Ever since I started learning Rails in late 2021, whenever I read the Rails Guides on Active Record Associations, my eyes always glazed over the topic of Polymorphic Associations. It always felt like some mysterious, enigmatic topic that only those working with complicated applications with complex domain models would use.
Finally, over the weekend, I finally decided to take a few hours in the afternoon and really learn how polymorphic associations work, and they turned out to be not so scary after all. The whole idea is actually pretty simple. If you don't want to read the whole 2800-word article, here's the gist of it:
A polymorphic association allows you to connect a model (e.g. Comment) to multiple models (e.g. Post, Video, and Photo) using a single belongs_to
association.
Hope that made sense. If not, keep reading. After you finish this article, you'll find that it's not as intimidating a topic as it sounds.
Note: Before you proceed, I assume that you're already familiar with associations in Active Record. You should know how to use has_one
, has_many
and belongs_to
to build a simple domain model. If not, first go read the basics of associations and then come back to this post. I'll wait.
Alright, with that out of the way, here's the list of topics we'll explore in this article.
What We'll Learn
- What is Polymorphism?
- A Practical Use Case for Polymorphic Associations
- Modeling Polymorphism
- One-to-One Polymorphic Association
- One-to-Many Polymorphic Association
- Conclusion
- Additional Resources
Sounds interesting? Let's get started by understanding the term 'Polymorphism'.
If you learned object-oriented programming in college, you must have heard about polymorphism. I never really understood it the first time and only learned it long after I had graduated and started working full-time as a developer.
In this section, I'll try to explain polymorphism the best I can, real quick.
What is Polymorphism?
Poly = Many, Morphe = Form
Polymorphism is the ability of an object to take on many forms.
Consider a simple example of Framework (doesn't matter if it's a class or an object), which can either be Rails or Laravel.
Both Rails and Laravel meet the IS-A condition with the Framework, i.e. Rails IS A Framework (in programming terms, Rails inherits from Framework). Hence the term Framework is polymorphic. That is, you can assign both instances of Rails and Laravel interchangeably to Framework at runtime.
// pseudo-code
Framework f;
f = new Rails();
f = new Laravel();
Here, a single object f
can have many forms. This is polymorphism.
Typically, the concept of polymorphism is used in statically typed languages like C# and Java, which have the concept of interfaces and abstract classes. However, it's important to remember that polymorphism is not limited to a specific programming language implementation but is also applicable in the real world.
For example, consider a person, which is a polymorphic term. This person can be a mother and wife at home, a CEO at work, and a coach for her kids' sports team. Depending on the situation, that single person can take many forms.
But what's the benefit of polymorphism, you ask?
The primary benefit of polymorphism is that it lets us refer to an entity in a generic (polymorphic) way, and use its different forms behind the scenes, depending on the circumstances.
If that statement just flew right over your head, don't worry. Let me explain it in programming terms you're familiar with.
Because a polymorphic variable can have multiple forms, you can use accept it in the parameters of a function or a dependency of a class, and pass that function any object (form) that this polymorphic variable can take. The same goes for the value returned by that function.
In the following example, the perform
function takes an argument of polymorphic type Framework
. Hence we can pass it an instance of either Rails
or Laravel
.
function perform(Framework f) {
// do something with the framework,
// regardless of whether it's Rails or Laravel
}
var rails = new Rails()
perform(rails)
var laravel = new Laravel()
perform(laravel)
The benefit of this approach is that as you keep adding new frameworks, they can all follow the abstract interface of the Framework, and the perform
function's definition doesn't have to change. Each concrete object will have its own implementation that can vary independently.
Alright, enough theory. Now you might be wondering how does this definition relate to polymorphic associations in Rails?
Let's think of a real problem you might come across while building real-world web applications.
A Practical Use Case for Polymorphic Associations
I strongly believe that you have to understand the problem before you rush to understand the solution to that problem. So let's think of a scenario where you'd need to use polymorphic associations.
Consider you're building a custom tutorial website, just like the one you're reading right now.
As of now, the website only supports posts, which are stored in the posts
table and represented in Ruby with the Post
Active Record model.
The readers of your site can comment on your posts, stored in the comments
table and represented by the Comment
model. The comments
table contains a post_id
column to refer to the post that this comment belongs to.
So far, so good.
After a while, you also want to record videos and upload them on the site. Additionally, the viewers should be able to comment on these videos, just like they can on the posts.
The videos will be stored in the videos
table with the Video
model. But what about the comments on the videos? Where should you store them and handle them in the code?
The first solution that comes to mind is to make the Comment
model belong_to
both Post
and Video
, by adding a nullable foreign key video_id
referencing the video. That is, the comments
table will have post_id
and video_id
columns, referencing the Post
and the Video
respectively.
class Comment < ApplicationRecord
belongs_to :post
belongs_to :video
end
Although it may work, there are a few problems with this solution.
- It's hard to extend. Imagine that in addition to a post and a video, now you want to add photos on the site, and users can comment on them, too. Now you've to write a new database migration to add a
photo_id
column and create a newbelongs_to :photo
association. - It complicates the validation. You have to add code to check that a comment never belongs to both a post and a video at the same time. That is, one of the
post_id
orvideo_id
columns will always be empty.
To be honest, it's not too big of a problem, at least for smaller applications. If these are the only three models that will ever need the comments, the above naive approach could work just fine.
However, you can imagine the growing pains as your application grows in complexity and you introduce new "commentable" models. The above solution won't scale.
To summarize the above pain points, let's state the problem we're trying to solve in specific terms:
How can we associate a single model with multiple models over time in a way that doesn't involve constantly changing the database schema for that model?
This is where polymorphic associations come into the picture.
Polymorphism To The Rescue
As we just learned, something is polymorphic if it can take multiple forms.
What if instead of having a comment belong to a post, a video, or a photo, it belonged to an abstract polymorphic entity, like a commentable?
Then commentable can take the form of either a post, a video, or a photo behind the scenes, and Comment doesn't have to worry about the concrete type it belongs to.
In other words, instead of maintaining the post_id
, video_id
, and photo_id
for each of the concrete types it may belong to, the comments table will only contain a single column called commentable_id
, referring to the exact post or video it belongs to.
However, having only the id of the associated model isn't enough. How will the comment know the type of model it belongs to?
To solve this, the comments
table would also need another column called commentable_type
, which will contain the name of the model this comment belongs to, i.e. Post
, Video
, or Photo
.
So now we've added two columns on the comments
table:
commentable_type
is the abstract term to refer to the type this comment belongs to, e.g.Post
orVideo
.commentable_id
referring to the ID of the record that this comment belongs to.
That's all that's needed in the database for the comments. The benefit of this approach is this:
You don't have to touch the comments
table again, no matter how many additional commentable models you'll add later. Just add the name of the type in commentable_type and id in commentable_id columns.
(Of course, you'll have to modify comments
table if you're adding new properties to the comment model...)
Now let's see what changes we need to make to our models to make the above solution work.
Modeling Polymorphic Associations in Rails
To reflect polymorphic associations in the Active Record models, we need to update both the parent and child models.
Parent Model: How can we reflect the fact that a Post (or a Video) has one or many comments associated with it?
Pass an option called as: :commentable
to the has_one
or has_many
association. It marks this model as commentable, indicating that it can have one or many comments.
class Post < ApplicationRecord
has_many :comments, as: :commentable
end
class Video < ApplicationRecord
has_many :comments, as: :commentable
end
Child Model: How can we reflect the fact that a comment is polymorphically associated with multiple other models?
Make it belong to a Commentable
, and pass an option called polymorphic: true
to the belongs_to
association.
class Comment < ApplicationRecord
belongs_to :commentable, polymorphic: true
end
From now on, any model in our application can support comments (making it commentable) without the need to alter the database schema or the Comment
model itself.
Note 1: The exact term commentable
doesn't matter. You can call it anything, as long as you use the same term in both models. But make it something meaningful.
Note 2: We don't need to create a Commentable
class. It's called commentable because it reflects the models (a post or video) that can have comments. Commentable is polymorphic, means the single term commentable can take the form of a post or a video.
Hope you're still with me and things are getting clearer. If something doesn't make sense, feel free to email me with your questions.
Let's solidify our understanding by looking at two concrete examples which show how polymorphic associations work in one-to-one and one-to-many relationships.
One-To-One Polymorphic Association
A one-to-one polymorphic association is similar to a typical one-to-one association. However, the main difference is that the child model can belong to more than one type of model using a single belongs_to
association.
Let's consider a different example than the one we've been using so far. Imagine that a Post
and a User
can have an image associated with them (a post has a featured image and the user has an avatar).
You can express this relationship via a polymorphic association to an Image
model. Using a one-to-one polymorphic relation allows you to have a single table of unique images associated with posts and users.
Table Structure
First, let's examine the table structure required to create this polymorphic relationship.
posts:
id: integer
name: string
users:
id: integer
name: string
images:
id: integer
url: string
imageable_id: integer
imageable_type: string
Note the imageable_id
and imageable_type
columns on the images
table. The imageable_id
column will contain the ID of the post or user, while the imageable_type
column will contain the class name of the parent model.
Rails uses the imageable_type
column to figure out the "type" of the parent model when accessing the imageable
relation. In this case, the imageable_type
column would contain either Post
or User
.
Database Migration
Let's add a migration that creates the images
table. As you can see, there is a column called imageable_type
that stores the class name of the associated object.
class CreateImages < ActiveRecord::Migration[7.0]
def change
create_table :images do |t|
t.string :url
t.integer :imageable_id
t.string :imageable_type
end
end
end
The migration API gives you a one-line shortcut with the references
method, which takes a polymorphic
option:
create_table :images do |t|
t.string :url
t.references :imageable, polymorphic: true
end
When generating a migration, you can use the following syntax:
bin/rails generate model image post:references{polymorphic}
Model Structure
Next, let's examine the Active Record models needed to build this relationship:
class Image < ApplicationRecord
belongs_to :imageable, polymorphic: true
end
class Post < ApplicationRecord
has_one :image, as: :imageable
end
class User < ApplicationRecord
has_one :image, as: :imageable
end
Retrieving the Polymorphic Associations
After defining your database table and models, you can access the associations via your models. For example, to get the image for a post, we can access the image
method on the Post
model.
post = Post.find(1)
image = post.image
You can also retrieve the parent of the polymorphic model by accessing the method named with the belongs_to
association. In this case, that is the imageable
method on the Image
model.
Hence, we will call the imageable
method to access the comment's parent model.
image = Image.find(1)
imageable = image.imageable
The imageable
method on the Image
model will return either a Post
or User
instance, depending on the model this image belongs to.
One-To-Many Polymorphic Association
A one-to-many polymorphic association is similar to a standard one-to-many association. Again, the main difference is that the child model can belong to more than one type of model using a single association.
For example, imagine the users of your application can "comment" on posts and videos. Using polymorphic associations, you'll use a single comments
table to contain comments for both posts and videos.
First, let's look at the table structure required to build this relationship.
Table Structure
posts:
id: integer
title: string
body: text
videos:
id: integer
title: string
url: string
comments:
id: integer
body: text
commentable_id: integer
commentable_type: string
Next, let's see the models needed to build this association.
Model Structure
class Comment < ApplicationRecord
belongs_to :commentable, polymorphic: true
end
class Post < ApplicationRecord
has_many :comments, as: :commentable
end
class Video < ApplicationRecord
has_many :comments, as: :commentable
end
Accessing the Association
Once your database table and models are defined, you can access the related associations as usual.
For example, to access all of the comments on a post, use the comments
method defined on the Post
model.
post = Post.find(1)
post.comments.each do |comment|
# ...
end
You can also get the parent of a polymorphic child model by accessing the name of the belongs_to
association.
In this case, that is the commentable
method on the Comment
model. So, we will access that method to access the comment's parent model.
comment = Comment.find(1)
commentable = comment.commentable
The commentable relation on the Comment
model will return either a Post
or Video
instance, depending on the type of comment's parent.
Conclusion
If you've stuck with me so far, you'll agree that polymorphic associations sound more intimidating than they really are.
To summarize what we've learned so far, polymorphic associations let us create a model that can belong to multiple models on a single association. You can also think of a polymorphic belongs_to
association as setting up an interface, e.g. commentable
or imageable
that any other model can use.
A polymorphic relationship allows the child model to belong to more than one type of model using a single association. Using a polymorphic association, we need to define a single belongs_to
association and add two related columns to the database table.
To wrap up, polymorphic associations help us solve a tricky problem in an elegant way.
Additional Resources
If you want to learn more about polymorphic associations, here're a few blog posts that I found helpful, which will help you solidify your understanding. If you know of any other good resources, let me know.
- Using Polymorphic Associations
- How to Use Polymorphic Associations in Rails
- Changing a polymorphic type in Rails
- Polymorphic Associations in Rails
That's a wrap. I hope you found this article helpful and you learned something new.
As always, if you have any questions or feedback, didn't understand something, or found a mistake, please leave a comment below or send me an email. I reply to all emails I get from developers, and I look forward to hearing from you.
If you'd like to receive future articles directly in your email, please subscribe to my blog. Your email is respected, never shared, rented, sold or spammed. If you're already a subscriber, thank you.