Bits for hackers.

Step by Step Guide to Polymorphic Associations in Rails

What are polymorphic associations

Typically, in Rails, when you need to make one class belong to multiple classes, you have to add a separate belongs_to method in the class each time. For example, if we think about the domain model of a generic social media web application, one can imagine that a comment can be made on a photo, a wall post, an event, an article, etc.

If we were to model this without polymorphic associations, we could do one of two things:

  1. Create a single comments model with multiple belongs_to for each association:
    1
    2
    3
    4
    5
    6
    
    class Comment < ActiveRecord::Base
      belongs_to :picture
      belongs_to :post
      belongs_to :event
      belongs_to :article
    end
    
    The problem with this method is that your table will have a number of foreign keys for each of the `belongs_to`; however, only one of them will actually have a value at one time - a comment can only belong to one of these other models at one time.
  2. Create multiple different comments models:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    
    class PictureComment < ActiveRecord::Base
      belongs_to :picture
    end
    
    class PostComment < ActiveRecord::Base
      belongs_to :post
    end
    
    class EventComment < ActiveRecord::Base
      belongs_to :event
    end
    
    class ArticleComment < ActiveRecord::Base
      belongs_to :article
    end
    
    Going this route, we no longer have the issue where we have a number of unused foreign keys; however, the issue is that there are now a lot of models that all more or less do the same thing and act the same way.

The solution to this is to use polymorphic associations with one model - this is a model can belong to more than one other model, on a single association.

How to create a polymorphic association

In order to create a polymorphic association, we create a comments model; however, rather than making it belong to each of the other classes, we make it belong_to a generic commentable object.

1
2
3
class Comment < ActiveRecord::Base
  belong_to :commentable, :polymorphic => true
end

Now, using this method, we can make any number of models “commentable” - meaning that they can be commented on. All we have to do is make each class have many comments:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Picture < ActiveRecord::Base
  has_many :comments, as: commentable
end

class Post < ActiveRecord::Base
  has_many :comments, as: commentable
end

class Event < ActiveRecord::Base
  has_many :comments, as: commentable
end

class Article < ActiveRecord::Base
  has_many :comments, as: commentable
end

A helpful tip

Using the polymorphic association method above, we now have only one comments model and our code is pretty DRY. However, the controller could still use some DRYing up. For example, when we’re looking to create a particular comment, we have to find the appropriate commentable to build the new comment with:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class CommentsController < ApplicationController
  def create
      if params[:picture_id]
          @commentable = Picture.find(params[:picture_id])
      elsif params[:post_id]
          @commentable = Post.find(params[:post_id])
      elsif params[:event_id]
          @commentable = Event.find(params[:event_id])
      elsif params[:article_id]
          @commentable = Article.find(params[:article_id])
      end

      @comment = @commentable.comments.build(comment_params)
  end
end

You can imagine that every time you need to find the commentable that the comment is associated with, you would have to run all of that conditional logic. However, Ryan Bates’ Railcast on polymorphic associations has a really nice method that can be added to DRY up your code in the following manner:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class CommentsController < ApplicationController    
  def create
      @commentable = find_commentable
      @comment = @commentable.comments.build(comment_params)
  end

  private

  def find_commentable
    params.each do |name, value|
      if name =~ /(.+)_id$/
        return $1.classify.constantize.find(value)
      end
    end
    nil
  end
end

This is a really nice piece of Ruby programming that uses regex to look through the params and find any params that end in “_id”. It then takes those params, uses the classify method to format the string to look like a class. It then uses the constantize method to find a declared constant with the name specified in the string. Finally, it looks through the class for the ID value in the key/value params hash.