DEV Community

mattIshida
mattIshida

Posted on

Polymorphic associations in Active Record

As a software developer, one of the best feelings has to be thinking up a great feature for your project, wondering anxiously if it can done, then finding it's a built-in feature of the library you're already working with. Read the docs--it pays!

In this blog post, I'll go over setting up polymorphic database relations in Active Record. Not only is it possible, it's surprisingly painless and useful.

Polymorphic???

The word "polymorphic" can be a bit of stumbling block. Let's quickly try to demystify it.

Normally, we think of a database relationships as modeled on something concrete and specific. If we have a table of episodes and a table of TV shows, how should the two tables be related?

To answer that question, we think about the real-world relationship of episodes and shows. An episode is, by definition, part of series linked together by a running theme and a core group of personnel behind the scenes. It's an expression of a piece intellectual property that can be thought of as a single entity, namely a show.

Thinking through that relation, it's clear that there is a unique show for each episode, whereas a show can (and ideally should) have many episodes.

This guides how we set up our Active Record migrations and models.

class Episode < ActiveRecord::Base
  belongs_to :show
end

class Show < ActiveRecord::Base
  has_many :episodes
end
Enter fullscreen mode Exit fullscreen mode

Episodes and shows are a very concrete relationship. So are ingredients and recipes, or employees and managers, or even Amazon users and orders.

But what about relations that are less concrete?

What about the relation of liking? I can like all sorts of things, almost anything at all, in fact, whether concrete or abstract. I can like strawberry ice cream or going to the movies or my last vacation or my neighbor's dog. I can like the show, or particular episodes, or the actors in the show, or even moments within the show.

If you tell me you have a sibling relation to some entity X, I know what kind of thing X is, namely a human being. If you tell me you like some entity X, I have no way of knowing what that is. At most, I can say it is the kind of thing that can be liked, which isn't much at all.

Liking is polymorphic. It's not particular about the kinds of things it relates.

Use cases

Some use cases for polymorphic relations should now be obvious.

A lot of Web 2.0 functionality naturally comes to mind. Likes, follows, comments, reactions, etc.

  • On a photo-sharing website, I want to be able to like photos but also comments on photos and maybe other users.
  • On movie review site, I should be able to like movies but also reviews of movies as well as directors and actors.
  • On a news website, I should be able to follow reporters but also topics.

The possibilities are endless.

In general: if you have a website about subject matter X, you will naturally want to enable user interactions with anything in that domain, and also with other users, and also with those other users' interactions.

Each user will have interactions with lots of different kinds of things.

Conceptual foundations

Fortunately, Active Record makes this surprisingly simple.

Before we get there, though, let's think about what we're asking for.

In our hypothetical likes table, each row or like-instance belongs to exactly one likable, or thing that is liked.

So we will need a column that tells us the id of the likable. Then we would also need some way of telling what kind of thing the likable is.

At this point, we could implement a separate likes table for reach kind of liked thing. Instead of just plain likes, we could have episodeLikes and showLikes and so on.

That would allow us to use vanilla has-many and belongs-to relations. But at the cost of multiplying the number of tables, and therefore of migrations and models as well. Whenever we wanted to enable likes on a new kind of thing, we would have to create a whole new table for its likes as well.

Not good.

Alternatively, we could try map particular ids to particular things, so that odd-numbered foreign keys indicated shows and even-numbered indicated episodes.

But this would meaning altering the logic by which ids are assigned. It would also make it difficult to add other likables without carefully rearranging the whole id-assignment logic.

Or we could somehow tell the database to look for the likable using an additional column, something that indicated the type of the likable. That way, Active Record would know which of many tables look in as well as the record within that table.

1) What kind of thing do you want?
2) What is its id?

Instead of a normal single-column foreign key, what we need conceptually is something like this:

likable_type
likable_id
Enter fullscreen mode Exit fullscreen mode

The idea is that both columns together function like a normal foreign key in pointing us to a particular record that the like belongs to.

Implementation

Without further ado, let's start setting up a polymorphic association from likes to shows and episodes.

1. Add in a type column to the belongs_to table

As mentioned, likes needs an additional column beyond just foreign key. So we'll need to modify the db migration to add that in. Our migration will look like this:

class CreateLikes < ActiveRecord::Migration[6.1]
  def change
    create_table :likes do |t|
      t.integer :user_id
      t.integer :likable_id
      t.string :likable_type
    end
  end
end
Enter fullscreen mode Exit fullscreen mode

If likes were only for shows, we could have include a show_id foreign key. But since we are abstracting likes to be free from any particular liked thing, we refer to the foreign key as likable_id. And since we still need to track what kind of thing is being liked, we add in a string column likable_type.

2. Use the :polymorphic option for belongs_to

Each like still belongs to exactly one likable, whether an episode or show. However, we need to tell Active Record that likables can be drawn from different tables.

To do this we simply add in an option in when we specify the belongs_to relation in our Like model.

class Like < ActiveRecord::Base
    belongs_to :user
    belongs_to :likable, polymorphic: true
end
Enter fullscreen mode Exit fullscreen mode

3. Use the :as option for has_many

Shows and episodes both have many likes. That part hasn't changed. We just need to specify that the likes for a particular show or episode should be found by looking in the likable_id and likable_type columns, as opposed to show_id or episode_id as in a typical association.

Active Record makes this easy to do with the :as option for has_many, where we simply pass in the abstract type :likable.

class Show < ActiveRecord::Base
    has_many :likes, as: :likable
end

class Episode < ActiveRecord::Base
    has_many :likes, as: :likable
end
Enter fullscreen mode Exit fullscreen mode

That's it! With a few simple options, we've implemented a polymorphic association in ActiveRecord. A like now belongs to exactly one likable, but that likable can be a show, an episode, or anything else we might need.

Working with polymorphic associations

Let's take a quick look at how easy it is to work with polymorphic associations.

Creating instances

We can test-drive our polymorphic association by creating some seed data.

The nice thing about Active Record is that it lets us pass in a hash of a user and likable.

Like.create(user: User.first, likable: Episode.first)
#=>#<Like:0x00007fdd89fc46d0 id: 1, user_id: 1, likable_id: 1, likable_type: "Episode">
Enter fullscreen mode Exit fullscreen mode

The likable_type column is filled in automatically with the appropriate string, along with the integer id.

The cool thing is that we can do this, even though we didn't create a separate model for Likable.

Instance methods

We can also call a #likes instance method on a user to see an array of likes.

User.first.likes
#=> [#<Like:0x00007fdd89f7e450 id: 1, user_id: 1, likable_id: 1, likable_type: "Episode">, #<Like:0x00007fdd89f7e0e0 id: 2, user_id: 1, likable_id: 1, likable_type: "Show">]
Enter fullscreen mode Exit fullscreen mode

What about going in the other direction from a likable, like an episode to an array of users who have liked it?

We can do that by adding a has_many and passing :likes to the :through option

class Episode < ActiveRecord::Base
    has_many :likes, as: :likable
    has_many :users, through: :likes
end
Enter fullscreen mode Exit fullscreen mode

Now we can call a #users method on an episode instance and see an array of users who have liked the episode.

Episode.first.users
#=> [#<User:0x00007fdd89f2e2e8 id: 1, name: "Marion Batz", email: "reba@hand.co">]
Enter fullscreen mode Exit fullscreen mode

Finally, what if we wanted to get likables of a particular type for each user?

We can do this quickly by adding one more line to our User model.

class User < ActiveRecord::Base
    has_many :likes
    has_many :liked_episodes, through: :likes, source: :likable, source_type: "Episode"
end
Enter fullscreen mode Exit fullscreen mode

Now we can call #liked_episodes and Active Record will focus only on those likes with the specified likable_type of "Episode".

User.first.liked_episodes
#=>[#<Episode:0x00007fdd9080c008 id: 1, num: 1, show_id: 1>]
Enter fullscreen mode Exit fullscreen mode

So without defining any new models, methods, or tables, we can establish polymorphic many-to-many relationships, with only a few optional parameters. Pretty cool!

Top comments (0)