Beyond has_many: Polymorphic associations in ActiveRecord

Skylar S
SkyTech
Published in
3 min readNov 29, 2018

Let’s say you are building Bookface, a social media app named after nothing in particular. Bookface has a simple model: Users write posts. But one day, Bookface adds a new feature: instead of only posting as a user, you can post as the owner of a page.

Now a post can belong to a user or a page! This is called a polymorphic association.

It’s important to remember that polymorphic associations can only be used when you have an instance that belongs to one model OR another model, not to both. Examples include:

  • Photos that are of a user, OR a product
  • An item that belongs to a store’s inventory, OR to a person’s belongings
  • A presentation that is given by a student, OR a teacher.

For now, let’s stick with Bookface.

ActiveRecord allows us to create a polymorphic association by adding two keys to our posts table. Rails convention is to call these values “postable_id” and “postable type”, but for the sake of clarity, I’m going to call these owner_id, and owner_type. The value of owner_type will either be “Page” or “User”, and that is how ActiveRecord will know who the owner of the post is.

class CreatePosts < ActiveRecord::Migration[5.2]
def change
create_table :posts do |t|
t.string :content
t.integer :owner_id
t.string :owner_type
t.timestamps
end
end
end

Now, we need to tell our models about these new columns.

In our post model file, we write:

class Post < ApplicationRecord
belongs_to :owner, polymorphic: true
end

Even though owner is not a model, this line will create an owner method that retrieves an instance whose id matches the :owner_id in the posts table, and whose type matches the owner type attribute (in this model, a string of either Page, or User) when we call Post.owner. Let’s continue setting up the relationship.

On the page model file:

class Page < ApplicationRecord
has_many :posts, as: :owner
end

Here we tell ActiveRecord that when we call Page.posts, it should return instances where the owner_id is equal to the page_id, and the owner_type is equal to page.

Finally, we do the same for our users model:

class User < ApplicationRecord
has_many :posts, as: :owner
end

That’s all we need to set up this association!

There is a problem with our model, however: a page posts, but it can also be posted on. You might say a page has many posts, and a post belongs to a page. This is a classic “has_many_through” relationship. Let’s try adding it.

So let’s add a page_id to our posts column, to associate it with a page.

class CreatePosts < ActiveRecord::Migration[5.2]
def change
create_table :posts do |t|
t.string :content
t.integer :owner_id
t.string :owner_type
t.integer :page_id
t.timestamps
end
end
end

In our user model, we can add our has_many_through relationship:

class User < ApplicationRecord
has_many :posts, as: :owner
has_many :pages, through: :posts
end

And in our Pages model … oh wait. This won’t work.

class Page < ApplicationRecord
has_many :posts, as: :owner
has_many :posts # Don't do this
end

We already defined posts. We’ll have to call the posts on the page something else. Luckily, ActiveRecord allows us to do this! All we have to do is specify a foreign key to match with, and tell it where to find that key. In this case, it’s the page_id on the post that tells us which page that post is… well, posted on.

class Page < ApplicationRecord
has_many :posts, as: :owner
has_many :posts_on, foreign_key: "page_id", class_name: "Post"
end

Our associations are now completed! But the posts method on our page is now a little vague. We’d have to remember that :posts mean the posts by the page, not posts on the page. For the sake of clarity, let’s change the name of that method as well. Instead of :posts, we’ll say :posts_by, so it reads:

class Page < ApplicationRecord
has_many :posts_by, as: :owner, foreign_key: "owner_id", class_name: "Post"
has_many :posts_on, foreign_key: "page_id", class_name: "Post"
end

Hooray! We can can now post to our hearts’ content.

--

--