Search code examples
ruby-on-railsrubyruby-on-rails-4activerecordpolymorphic-associations

What is the Rails way to work with polymorphic associations?


I have few models in my Rails application, which are:

  1. User
  2. Photo
  3. Album
  4. Comment

I need to make comments belog to either to Photo or Album, and obviously always belong to User. I'm going to use polymorphic associations for that.

# models/comment.rb

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

The question is, what is the Rails way to describe #create action for the new comment. I see two options for that.

1. Describe the comment creation in each controller

But ths is not a DRY solution. I can make one common partial view for displaying and creating comments but I will have to repeat myself writing comments logic for each controller. So It doesn't work

2. Create new CommentsController

This is the right way I guess, but as I aware:

To make this work, you need to declare both a foreign key column and a type column in the model that declares the polymorphic interface

Like this:

# schema.rb

  create_table "comments", force: :cascade do |t|
    t.text     "body"
    t.integer  "user_id"
    t.integer  "commentable_id"
    t.string   "commentable_type"
    t.datetime "created_at",       null: false
    t.datetime "updated_at",       null: false
  end

So, when I will be writing pretty simple controller, which will be accepting requests from the remote form:

# controllers/comments_controller.rb

class CommentsController < ApplicationController
  def new
    @comment = Comment.new
  end

  def create
    @commentable = ??? # How do I get commentable id and type?
    if @comment.save(comment_params)
      respond_to do |format|
        format.js {render js: nil, status: :ok}
      end
    end
  end

  private

  def comment_params
    defaults = {:user_id => current_user.id, 
                :commentable_id => @commentable.id, 
                :commentable_type => @commentable.type}
    params.require(:comment).permit(:body, :user_id, :commentable_id, 
                                    :commentable_type).merge(defaults)
  end
end

How will I get commentable_id and commetable_type? I guess, commentable_type might be a model name.

Also, what is the best way to make a form_for @comment from other views?


Solution

  • You'll be best nesting it in the routes, then delegating from the parent class:

    # config/routes.rb
    resources :photos, :albums do
       resources :comments, only: :create #-> url.com/photos/:photo_id/comments
    end
    
    # app/controllers/comments_controller.rb
    class CommentsController < ApplicationController
       def create
          @parent  = parent
          @comment = @parent.comments.new comment_params
          @comment.save
       end
    
       private
    
       def parent
          return Album.find params[:album_id] if params[:album_id]
          Photo.find params[:photo_id] if params[:photo_id]
       end
    
       def comment_params
          params.require(:comment).permit(:body).merge(user_id: current_user.id)
       end
    end
    

    This will fill it out automatically for you.


    In order to give yourself a @comment object, you'll have to use:

    #app/controllers/photos_controller.rb
    class PhotosController < ApplicationController
       def show
          @photo = Photo.find params[:id] 
          @comment = @photo.comments.new
       end
    end
    
    #app/views/photos/show.html.erb
    <%= form_for [@photo, @comment] do |f| %>
      ...