BETA

Railsでポリモーフィック関連付けとajaxを使ってコメント機能を実装

投稿日:2019-12-27
最終更新:2019-12-27

タイトルの通りです。
コメント機能が実装できたので、その方法を解説していきたいと思います。

少々javascript(というかjquery)の知識も必要ですが、あんま難しいことはしてないので大丈夫です。
なお、Railsでjqueryが使える前提で話を進めていきます。
インストールの仕方は下記記事を参考にしてみてください。
RailsでjQueryを使えるようにする方法

rails 6 の場合はjqueryのインストール方法がちょっと違います。
以下記事を参考にしてください。

Rails 6 でjQueryを使う

そもそもポリモーフィック関連付けとは?

簡単に言うと、一つのモデルに対して複数のモデルを関連づけるとき、共通のインターフェースを提供する機能です。

例えば、BookモデルとReportモデルがあるとします。
そして今回はこの二つにコメント機能をつけるとします。
そのためにはCommentモデルを以下のようにこの二つに結びつける必要があります。

1 Book - Comment 多
1 Report - Comment 多

これはモデルが増えるたびに関連付けを行わなきゃいけないので、面倒です。
そこでCommentableという共通のインターフェースを用意して、それにCommentを関連付けます。

1 Commentable(Book or Report or ...) - Comment 多

この通り、一つの関連付けで済みます。
これがポリモーフィック関連付けです。

詳しくは以下の記事が大変わかりやすかったです。
【初心者向け】Railsのポリモーフィック関連付けを理解しよう

マイグレーション

まずはComment モデルを作成していきます。
以下のコマンドを実行してください。

$ rails g model comment content:text commentable:references{polymorphic}  

これで以下のようなマイグレーションファイルが作成されるはずです。

# app/db/***_create_comment.rb  
class CreateComments < ActiveRecord::Migration[6.0]  
  def change  
    create_table :comments do |t|  
      t.text :content, null: false  
      t.references :commentable, polymorphic: true, null: false  

      t.timestamps  
    end  
  end  
end  

t.references :commentable, polymorphic: true でちゃんとCommentableに対しての外部キーとポリモーフィックの設定がされていますね。

忘れずmigrateしましょう。

$ rails db:migrate  

ポリモーフィック関連付け

次はモデル間で関連付けを行っていきます。
以下のように記述してください。

# app/models/book.rb  
class Book < ApplicationRecord  
  has_many :comments, as: :commentable  
end  

# app/models/report.rb  
class Report < ApplicationRecord  
  has_many :comments, as: :commentable  
end  

# app/models/comment.rb  
class Comment < ApplicationRecord  
  belongs_to :commentable, polymorphic: true  
end  

上記でBookReportそれぞれに対して、CommentableとしてCommentが関連付けられました。

ビューにコメント部分を追加

次にBookReportのそれぞれの詳細画面に、コメント一覧とコメント投稿フォームを追加していきます。

app/views/commentsに以下3つのパーシャルファイルを作成してください。

<%# app/views/_comments.html.rb %>  
<div>  
  <h3>コメント一覧</h3>  
  <div id='comment_area'>  
    <%= render 'comments/comment', comments: commentable.comments %>  
  </div>  
</div>  

<div id="flash_messages">  
</div>  

<div>  
  <h3>投稿する</h3>  
  <div id="comment_form">  
    <%= render 'comments/form', commentable: commentable, comment: comment %>  
  </div>  
</div>  


<%# app/views/comments/_comment.html.rb %>  
<% comments.each do |comment| %>  
<div id="comment" data-comment="<%= comment.id %>">  
  <p>コメント:<%= comment.content %></p>  
  <% if comment.user == current_user %>  
  <p>  
    <%= link_to '編集する', [:edit, comment.commentable, comment], remote: true %>  
    <%= link_to '削除する', [comment.commentable, comment], method: :delete, remote: true, data: { confirm: 'Are you sure?' } %>  
  </p>  
  <% end %>  
</div>  
<% end %>  


<%# app/views/comments/_form.html.erb %>  
<%= form_with(model: [commentable, comment]) do |f| %>  
  <%= f.text_area :content %>  
  <div class="actions">  
    <%= f.submit "コメントをする", class: 'btn btn-warning px-5' %>  
  </div>  
<% end %>  

link_toの部分でremote: trueを設定することで ajax で通信するようになります。
form_withではデフォルトで ajax がオンなので何も書いていませんが、明示したい場合は以下のように書いてもいいです。

<%= form_with(model: [commentable, comment], remote: true) do |f| %>    

app/views/comments/_comment.html.rbdata-comment="<%= comment.id %>"の部分は、あとで ajax で使うために記述しています。

また通常はbook_comment_pathのように記述するのですが、commentableの中身が変わるため、[commentable, comment]のように記述してリンク先のURLを取得しています。
Rails polymorphic nested route generation

最後にBookReportのそれぞれの詳細画面で、さっきのパーシャルファイルを読み込みます。
以下をそれぞれのshow.html.erbファイルで追加してください。

<%# app/views/books/show.html.erb %>  
<%= render 'comments/comments', commentable: @book, comment: current_user.comments.build %>  

<%# app/views/reports/show.html.erb %>  
<%= render 'comments/comments', commentable: @report, comment: current_user.comments.build %>  

ルーティング

CommentBookReportのどちらに属しているかを判断するために、ネストしたルーティングを記述する必要があります。

# app/config/routes.rb  
resources :books do  
  resources :comments, only: %i[create edit update destroy], module: :books  
end  

resources :reports do  
  resources :comments, only: %i[create edit update destroy], module: :reports  
end  

上記では、module:でそれぞれ使うコントローラを分けています。
この意味は次のコントローラを作成していく時に分かります。

コントローラ

以下のコントローラを作成してください。

# app/controllers/comments_controller.rb  
class CommentsController < ApplicationController  
  prepend_before_action :set_commentable, only: %i[create edit update destroy]  
  before_action :set_comment, only: %i[edit update destroy]  

  def create  
    @comment = @commentable.comments.build(comments_params)  
    @comment.user = current_user  

    if @comment.save  
      respond_to do |format|  
        format.js { flash.now[:success] = t('flash.new') }  
      end  
    else  
      @commentable.comments.destroy(@comment)  
      respond_to do |format|  
        format.js { flash.now[:danger] = @comment.errors.full_messages[0] }  
      end  
    end  

    render :index  
  end  

  def edit  
  end  

  def update  
    if @comment.update(comments_params)  
      respond_to do |format|  
        format.js { flash.now[:success] = t('flash.update') }  
      end  
      render :index  
    else  
      respond_to do |format|  
        format.js { flash.now[:danger] = @comment.errors.full_messages[0] }  
      end  
      render :edit  
    end  
  end  

  def destroy  
    if @comment.destroy  
      respond_to do |format|  
        format.js { flash.now[:success] = t('flash.destroy') }  
      end  
    end  

    render :index  
  end  

  private  
    def set_comment  
      @comment = @commentable.comments.find(params[:id])  
    end  

    def comments_params  
      params.require(:comment).permit(:content)  
    end  
end  

ajax で通信するので、format.jsを使ってフラッシュメッセージをセットしています。
また、create,edit,destroyで共通してindex.js.erbファイルを使うようにしています。

次に以下二つのコントローラを作成してください。

# app/controllers/books/comments_controller.rb  
class Books::CommentsController < CommentsController  
  private  
    def set_commentable  
      @commentable = Book.find(params[:book_id])  
    end  
end  

# app/controllers/reports/comments_controller.rb  
class Reports::CommentsController < CommentsController  
  private  
    def set_commentable  
      @commentable = Report.find(params[:report_id])  
    end  
end  

こういう風にモデルごとにコントローラを分けることで、@commentableの中身を動的にセットできるようになります。
CommentsControllerを継承しているので、通常通りにルーティングに従って処理することもできます。
ルーティングのところで設定したmodule:の部分は、こういう風に分けるために行いました。

上記の記述は以下の動画を参考にしました。
Comments With Polymorphic Associations

ビュー

ajax で通信するときは、indexアクションはindex.html.erbではなくindex.js.erbをレンダリングするようになります。
また、js.erbファイルではjavascript(jquery)が記述できます。

ですので、以下のようにindex.js.erbを作成します。

<%# app/views/comments/index.js.erb %>  
$('#comment_area').html("<%= j(render 'comments/comment', comments: @commentable.comments ) %>");  

<% flash.each do |type, message| %>  
$("#flash_messages").html('<div class="alert alert-<%= type %>"><%= message %></div>');  
<% end %>  

$('#comment_form').html("<%= j(render 'form', commentable: @commentable, comment: current_user.comments.build) %>");  

少しだけ解説します。

まず$('#comment_area').htmlのように書くことで、idがcomment_areaである部分の中身を書き換えられます。
上の場合だと、renderメソッドでパーシャルファイルをレンダリングして、中身をそれで書き換えています。

また、jメソッドはescape_javascriptの省略形です。
単にjavascript用にエスケープ処理を施してます。

$('#comment_form')部分では、comment変数にからのcommentインスタンスを渡してフォームの部分を書き換えています。

次に編集用のedit.js.erbを作成します。

<%# app/views/comments/edit.js.erb %>  
$('#comment[data-comment="' + <%= @comment.id %> + '"]').html("<%= j(render 'form', commentable: @commentable, comment: @comment) %>");  

<% flash.each do |type, message| %>  
$("#flash_messages").html('<div class="alert alert-<%= type %>"><%= message %></div>');  
<% end %>  

$('#comment[data-comment="' + <%= @comment.id %> + '"]')の部分でどのコメントをクリックしたのかを判断して、中身をフォームで書き換えています。
data-commentの部分はここで用いるために用意しました。

これでコメント機能の実装は完了です!!
お疲れ様でした♂

今回学んだこと

  • ポリモーフィック関連付けのやり方
  • ネストしたルーティングの記述方法
  • 適切なコントローラ処理の振り分け方
  • ajax通信のやり方
  • jqueryでの記述方法

などなど、いろんなことが勉強できました。
やはり、Railsはまだまだ奥深いです・・

次はajaxでのフォロー機能の実装方法を解説する予定です。

参考にしたサイト等

ポリモーフィック関連のコントローラー
form_forをremote: trueでAjax実装
RailsでAjaxを使ってコメントの編集方法
Rails 4でcallback(before/after/around action)の実行順序おさらい

技術ブログをはじめよう Qrunch(クランチ)は、プログラマの技術アプトプットに特化したブログサービスです
駆け出しエンジニアからエキスパートまで全ての方々のアウトプットを歓迎しております!
or 外部アカウントで 登録 / ログイン する
クランチについてもっと詳しく

この記事が掲載されているブログ

BЯunoの技術ブログ。日々学んだことを記録していくよ。

よく一緒に読まれる記事

0件のコメント

ブログ開設 or ログイン してコメントを送ってみよう