Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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?

like image 348
Viktor Avatar asked Jan 27 '16 12:01

Viktor


People also ask

How is polymorphic association set up in Rails?

The basic structure of a polymorphic association (PA)sets up 2 columns in the comment table. (This is different from a typical one-to-many association, where we'd only need one column that references the id's of the model it belongs to). For a PA, the first column we need to create is for the selected model.

What is polymorphic association Rails?

Polymorphic relationship in Rails refers to a type of Active Record association. This concept is used to attach a model to another model that can be of a different type by only having to define one association.

How do I create a polymorphic object in Rails?

The first thing we need to do is to use the Rails generator to generate a model and a migration for the Comment model. The references definition will automatically create columns called commentable_type and commentable_id . You don't need to worry about these columns as Rails will take care of them for you.

What is polymorphic association Ruby?

2.9 Polymorphic Associations A slightly more advanced twist on associations is the polymorphic association. With polymorphic associations, a model can belong to more than one other model, on a single association. For example, you might have a picture model that belongs to either an employee model or a product model.


1 Answers

I would use nested routes together with good old inheritance.

Rails.application.routes.draw do


  resources :comments, only: [:show, :edit, :update, :destroy] # chances are that you don't need all these actions...

  resources :album, shallow: true do
    resources :comments, only: [:new, :index, :create],  module: 'albums'
  end

  resources :photos, shallow: true do
    resources :comments, only: [:new, :index, :create],  module: 'photos'
  end
end

class CommentsController < ApplicationController
  before_action :set_commentable, only: [:new, :index, :create]

  def create
     @comment = @commentable.comments.new(comment_params) do |c|
       c.user = current_user
     end
     # ...
  end

  # ...
end

class Albums::CommentsController < ::CommentsController
  private
    def set_commentable
      @commentable = Album.find(param[:id])
    end
end

class Photos::CommentsController < ::CommentsController
  private
    def set_commentable
      @commentable = Photo.find(param[:id])
    end
end

While you could simply let CommentsController look at the parameters to determine what the "commentable" resource is I rather prefer this solution as CommentsController otherwise ends up dealing with far more than a single resource and can swell into a god class.

This really shines when you have index actions and need to perform joins based on the parent resource or when things just get complicated.

Making a reusable form is quite simple using partials:

# views/comments/_form.html.erb
<%= form_for([commentable, commentable.comment.new]) do |f| %>
  <%= f.label :body %>
  <%= f.text_field :body %>
<% end %>

You would then include it like so:

<%= render partial: 'comments/form', commentable: @album %>
like image 83
max Avatar answered Oct 19 '22 01:10

max