Polymorphic Associations in Rails

Polymorphic Associations in Rails: Why, What, and How

Polymorphic associations in Rails allow a single model to belong to multiple models. This article covers them in-depth. We'll start with understanding the concept of polymorphism, learn what a polymorphic association is, why we need them, how they work, and how to use them in your Rails application.

11 min read

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

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.

To learn about polymorphism in-depth, read the excellent answers posted on this question on StackOverflow: What is polymorphism?

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 (or constructor) any object (form) that this polymorphic variable can take. The same goes for the value returned by that function.

function perform(Framework f) {
  // do something with the framework, 
  // regardless of whether it's Rails or Laravel
  
  return new Rails()
}

Framework f = perform(new Rails())

Framework f = perform(new 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 needn't 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.

post with comments
post with comments

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.

💡
We've to make both the foreign keys nullable because a comment can't belong to both post and video at the same time, and one of the columns will always be empty.
class Comment < ApplicationRecord
  belongs_to :post
  belongs_to :video
end
post and video with comments
post and video with comments

Although it may work, there are a few problems with this solution.

  1. 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 new belongs_to :photo association.
  2. 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 or video_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, here's the problem we're trying to solve:

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 or Video.
  • commentable_id referring to the ID of the record that this comment belongs to.
commentable polymorphic association
commentable polymorphic association

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.

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

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.

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

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 Association

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 starndard 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.

That's a wrap. I hope you liked this article 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 look forward to hearing from you.

If you'd like to receive future articles directly in your email, please subscribe to my blog. If you're already a subscriber, thank you.