Understanding Polymorphic Associations in Rails Through a Case Study
Tags: rails, ruby, polymorphism, database
Published: 2024-07-16 13:21:04

I recently got to implement a polymorphic table in Rails at work. I've read about polymorphism a few times in books or blogs, but I never quite understood it. Having a real-life situation to apply the concept to at work, along with some conversations with my team's lead engineer finally made it click!

A lot of the examples in other articles and in even in the documentation are somewhat contrived, so I'd like to share what I did on the job as a real-world mini case study in hopes that this helps someone else. 

## The situation

My company provides employee benefits (e.g. dental and vision insurance) for other companies and we have many types of users that can interact with our platform such as company HR admins, brokers who sell insurance, and the members who have insurance. Naturally, we need to send a lot of emails to these people. The goal of my work project was to consolidate the different email categories we have and better keep track of the recipients for emails that we send out. For example, we have emails related to invoicing and insurance renewals, and they are sent to different people depending on their role within the system.

In regards to modeling, I needed to model a table that represented recipients of a set of emails under a certain category, where one category like "Invoicing" could contain several different specific emails. I named the model `EmailCategoryRecipient`, and each "recipient" could be associated with different kinds of user profiles we have in the system: a `CompanyContact` (e.g. an HR person at a company) and a `BrokerProfile` (e.g. the broker who sold the insurance to the company). An `EmailCategoryRecipient` belongs to a profile, where the profiles could be different classes.

Considering the model relationships, it sounded exactly like the kind of situation a model with a polymorphic association could help with!

## The polymorphic association and database table

### Creating the polymorphic association

A polymorphic association lets you create a model that can belong to more than one class, but uses only one `belongs_to` association instead of multiple. This multifaceted association is usually represented by the name of the concept + the `-able` prefix. In my case, the associations were different kinds of profiles so I called it `profileable`, but you can name these associations anything you want.

Here's what it would look like to set up the polymorphic relationship in the models I mentioned above:

```ruby
class EmailCategoryRecipient < ApplicationRecord
  belongs_to :profileable, polymorphic: true
end 

class CompanyContact < ApplicationRecord
  has_many :email_category_recipients, as: :profileable
end

class BrokerProfile < ApplicationRecord
  has_many :email_category_recipients, as: :profileable
end
```

Again, even though `EmailCategoryRecipient` only has one `belongs_to` for `profileable`, it can represent different types of classes—a `CompanyContact` or  a `BrokerProfile`. This is possible because I used the `polymorphic: true` option on the `belongs_to` association. As you can imagine, there could be many other things that need to have email recipients associated to them as well, like a `Member` or an `Admin` profile. Any number of these associations could be captured in the polymorphic `profileable` attribute.

### Creating a migration to set up the polymorphic association

To make the above relationships work, you need to create a migration that sets up a table that can handle the polymorphic associations. Rails gives us handy shortcuts to do so. It would look something like this:

```ruby
class CreateEmailCategoryRecipients < ActiveRecord::Migration[7.1]
  def change
    create_table :email_category_recipients do |t|
      t.string :category_name # just to keep track of category, this has nothing to do with the polymorphic piece
      t.references :profileable, polymorphic: true

      t.timestamps
    end
  end
end
```

Even though it's only one line, the `t.references :profileable, polymorphic: true` will actually create two columns and an index. The columns created are an `*_id` column and `*_type` column which are based on the polymorphic name. (in this case `profileable`). This one-liner is a nice shortcut method, but you could also write the migration explicitly like below. It would be equivalent to the migration above:

```ruby
class CreateEmailCategoryRecipients < ActiveRecord::Migration[7.1]
  def change
    create_table :email_category_recipients do |t|
      t.string  :category_name # again, this is just an additional attribute not related to polymorphic columns
      t.bigint  :profileable_id
      t.string  :profileable_type

      t.timestamps
    end

    add_index :email_category_recipients, [:profileable_type, :profileable_id]
  end
end
```

Let me explain the two columns `profileable_id` and `profileable_type`. The `profileable_id` holds the `id` (primary key) of the associated object while the `profileable_type` keeps track of what kind of object it is, which is the stringified class name of that object (`"CompanyContact"` or `"BrokerProfile"` in this case). We need the `*_type` column in addition to the `*_id` column because Rails needs to know what database table to look in to fetch the associated record, as the different types are stored in separate database tables. When you assign an object as a `profileable`, Rails will take care of filling out these two columns automatically with that object's id and class.

### Something to think about: Polymorphism is not the only way to do this

While polymorphic relationships are pretty cool and can provide an elegant solution, I want to call out that you don't actually _need_ to use it every time you have to associate multiple similar classes to a model. In fact, there are a lot of times when it's not the right tool for the job.

The alternative is to just associate each thing separately with the standard `belongs_to` and respective `*_id` column.

Modeling `EmailCategoryRecipient` as a non-polymorphic relationship would look like this:

```ruby
class EmailCategoryRecipient < ApplicationRecord
  belongs_to :company_contact
  belongs_to :broker_profile
end

class CompanyContact < ApplicationRecord
  has_many :email_category_recipients
end

class BrokerProfile < ApplicationRecord
  has_many :email_category_recipients
end
```

The migration for the above setup would look like this:

```ruby
class CreateEmailCategoryRecipients < ActiveRecord::Migration[7.1]
  def change
    create_table :email_category_recipients do |t|
      t.string :category_name
      # t.belongs_to is another shortcut method that will create indices for you too, unless specified not to
      t.belongs_to :company_contact
      t.belongs_to :broker_profile

      t.timestamps
    end
  end
end
```

Or alternatively with out using the shortcut `t.belongs_to`:

```ruby
class CreateEmailCategoryRecipients < ActiveRecord::Migration[7.1]
  def change
    create_table :email_category_recipients do |t|
      t.string :category_name
      t.bigint :company_contact_id
      t.bigint :broker_profile_id
      
      t.timestamps
    end

    add_index :email_category_recipients, :company_contact_id
    add_index :email_category_recipients, :broker_profile_id
  end
end
```

If you were to go this route, you would need to assign any `BrokerProfile` objects directly to the `EmailCategoryRecipient`'s `broker_profile` attribute (or the id to the `broker_profile_id`) and the same goes for `CompanyContact`.

## When would you NOT want to use polymorphism

The big benefits of using polymorphic associations are that it helps keep the code clean, simple, and flexible. 

That being said, I think that polymorphism can also make your code more abstract and therefore harder to understand at a glance, especially for people who aren't familiar with how polymorphic tables work or for someone who doesn't yet understand that model's relationship to other models. For example, it's not immediately obvious what classes `profileable` should refer to unless you just "know" from the get go. You may need to go searching through the codebase to find what classes can be associated with the polymorphic one.

Another thing to think about is whether the polymorphic objects you plan to associate with polymorphic type are similar or not. That is, do they share similar interface (attributes and methods) in places where it matters? It depends on the use case, but it may be extra hassle if the different classes aren't similar in their interface. 

For example, if you needed to pull email off of `CompanyContact` and `BrokerProfile` but one model used the attribute name `email` and the other `email_address`, you would have to check the class type to figure out what method could be called, alias the method in one class to match the other, or come up with another solution to avoid getting errors if the wrong method gets called on the wrong object. If you need to do this for multiple attributes on these classes, it may defeat the purpose of using a polymorphic association in the first place.

You could also run into data integrity issues if the "wrong" kind of object gets saved into the polymorphic table or if a non-existent object gets saved into the table. With polymorphic associations, Rails does not check the referential integrity of the record, that is, does it actually exist in the database. Queries will generally be slower as well, since Rails needs to check both the id and the type of the object. 

## The final outcome

To bring this case study to an end, I want to touch on the final decision I made, which was actually _not_ to use the polymorphic association. I ended up using explicit id columns for each associated object instead. Though I had already migrated the new tables, it was easy to changed because we hadn't started using them yet.

There are a couple reasons why I went the other direction. The main reason is that I found out we needed to have an additional association with the `EmailCategoryRecipient` model that was called `PlatformConfig`. The purpose of this model is to store configurations of the insurance partner platforms we work with, including the contact email for that partner. For our company, it represents another kind of recipient, but it is not a type of profile, and therefore it didn't make sense to consider it a `profileable` type. It also seemed awkward to have both a polymorphic entity for profiles and a separate non-polymorphic entity for the platforms.

The second reason is that the `CompanyContact` and `BrokerProfile` models were originally created at different times for different purposes. `CompanyContact` was implemented long before our concept of a user "profile" was even introduced. For our use case, it would have been ideal if we could "trust" that the different `profileable` objects have similar interfaces, but that wasn't necessarily the case, so we decided the explicit `belongs_to` relationships were more appropriate. 

One of the drawbacks here is that when we need to associate another type of profile to `EmailCategoryRecipient` in the future, we will need to change the table in order to add another `belongs_to` relationship. On the other hand, having explicit relationships makes it more obvious what kind of object we are dealing with at any given time.

## References

- 
[Rails documentation](https://guides.rubyonrails.org/association_basics.html#polymorphic-associations)