Polymorphism in 6 steps

A slightly advanced association in ActiveRecord is called polymorphic association. This usually happens when a model can belong to more than one model, even if it behaves the same way, but it’s easier to understand with an example.

Suppose you have a blog that has articles, photos, and events. You want to add comments on all of these, but the comments will all belong to a different kind of model (article/photo/event). So let’s start.

1. Set up/generate the comments model.

class CreateComments < ActiveRecord::Migration
  def change
    create_table :comments do |t|
      t.string  :body
      t.integer :commentable_id
      t.string  :commentable_type
      t.timestamps
    end
  end
end

 

One this here to note. The commentable_id and commentable_type will store information about the article/photo/event. The word “commentable” can literally be anything you want.

2. Set up the associations with the models.

You will need to set up the associations in ALL the models that will be related to this association.

The Comment model should have the belongs_to association and the polymorphic flag checked as true:

class Comment < ActiveRecord::Base
  belongs_to :commentable, polymorphic: true
end

 

And now the rest of the models should have the has_many associations:

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

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

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

 

This will let you retrieve comments with @photo.comments, @article.comments, @event.comments.

3. Make sure the routes.rb file is configured properly.

  resources :articles do
    resources :comments
  end

  resources :photos do
    resources :comments
  end

  resources :events do
    resources :comments
  end

 

You essentially need a nested route of the Comment class inside each class in your app. This way your routes will look like this:

/articles/:article_id/comments/:id(.:format)

(and so forth)

4. Set up the Comments controller

In the Comment controller, do the following:

Set up a private method to find the commentable:

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

What we’re doing here is:
a. Params come in a hash, therefore we’re iterating through key-value pairs
b. If the key matches the regex /(.+)_id$/ (ex. “article_id”):
c. Return:
c.1 $1 is the value stored in the regex above (putting a parenthesis makes a match group)
c.2 .classify creates a class NAME, however, in a string – “article” is now “Article”
c.3 .constantize will convert it to an actual class.
Therefore: Article.find(4) -> since value is the actual value of the key-hash pair.

Now all we need is to put a before_filter in the controller:

before_filter :find_commentable

 

As for the rest of the actions:

  def index
    @comments = @commentable.comments
  end

  def new
    @comment = @commentable.comments.new
  end

  def create
    @comment = @commentable.comments.new(params[:comment])
    if @comment.save
      redirect_to @commentable, notice: "Comment created."
    else
      render :new
    end
  end

 

5. Set up the view for the Comment form.

_form.html.erb partial would start off like this:

<%= form_for [@commentable, @comment] do |f| %>
    <%= f.text_area :content %>
    <%= f.submit %>
<% end %>

 

and the link_to for a new comment would have the following params:

<%= link_to "New Comment", [:new, @commentable, :comment] %>

 

6. Set up the “show” action for the rest of the controllers

All you need here now is to know how things relate to each other. For example, in our Article controller:

def show
    @article = Article.find(params[:id])
    @commentable = @article
    @comments = @commentable.comments
    @comment = Comment.new
  end

 

And then you do the rest for the other models, following the same logic.

Remember to play around in the console! It’s fun and it’ll make things clearer. Especially to understand a method like the one above, do this:

article = Article.find(2).comments

and you’ll get to see how the magic works!

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes:

<a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>