見出し画像

Rails コメント機能

今回はコメント機能を作っていきます。
コメント機能とは何らかのデータに対してコメントができるものになります。
本記事は以下のマガジンで実装した内容を引き継いでいますが、以下のマガジンの内容を実装していなくても問題ありません。

コメントテーブルを追加

まずはコメントテーブルを追加します。
本記事では、コメントはuserとbookに紐づくようにしますが、お好みのデータに紐づくように実装していただいて問題ありません。
まずは以下のコマンドを実行してモデルを追加します。

bundle e rails g model comment body:text user:reference book:reference

コメントの内容を保存するカラムは bodyとしました。たまに comments テーブルに comment カラムを追加する記事を見かけますがカラム名とテーブル名が被っていてわかりづらいので避けた方が良いでしょう。
以下のコマンドを実行し、テーブルの作成を行います。

bundle e rails db:migrate

モデルを作成するだけではテーブルは作成されません。必ず migrate を実行するようにしましょう。

テーブルを作成する際は必ず rails db:migrate コマンドを実行しなければならない

以下のようなテーブルが作成されていれば成功です。
テーブルが作成済みかどうかは schema.rb を見て確認します。

  create_table "comments", force: :cascade do |t|
    t.text "body"
    t.integer "user_id", null: false
    t.integer "book_id", null: false
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
    t.index ["book_id"], name: "index_comments_on_book_id"
    t.index ["user_id"], name: "index_comments_on_user_id"
  end

これでテーブルの作成は完了です。

コメントを登録するためのURLをつくる

次に、コメントを登録する際のURLを作成します。
config/routes.rb を修正します。本記事の場合は以下のように修正します。

  resources :books do
    ...
    resources :comments, only: [:create, :destroy]
  end

作成されたURLを確認してみます。以下のコマンドを実行します。

bundle e rails routes -g comment

以下のように出力されていれば成功です。

       Prefix Verb   URI Pattern                            Controller#Action
book_comments POST   /books/:book_id/comments(.:format)     comments#create
book_comment DELETE /books/:book_id/comments/:id(.:format) comments#destroy

今回はコメントを作成、削除する機能しか追加する予定がないため、create と destroy のURLのみを追加しています。

has_many を追加する

紐付けたいデータに対して has_many を追加します。

class Book < ApplicationRecord
  ...
  has_many :comments, dependent: :destroy
  ...
end
class User < ApplicationRecord
  ...
  has_many :comments, dependent: :destroy
end

コメントを登録するフォームをつくる

次に、コメントを登録するためのフォームを作成します。
お好みのページに作成してください。今回は book の詳細ページにフォームを追加します。

<%= form_with local: true do |form| %>
  <%= form.text_area :body %>
  <%= form.submit '作成' %>
<% end %>

現在の状態では入力欄にテキストを入れて作成ボタンを押しても何も起こりません。コメントの作成を可能な状態にするため、徐々に修正していきます。まずはformに model の指定を追加します。

<%= form_with model: @comment, local: true do |form| %>
  <%= form.text_area :body %>
  <%= form.submit '作成' %>
<% end %>

@comment という変数を追加しました。この変数はどこにも定義していないため、コントローラ側に変数の定義を追加します。

  def show
    @book = Book.find(params[:id])
    @comment = current_user.comments.new(book: @book)
  end

次に、コメントを登録するためのURLのオプションを追加します。URLは以下のコマンドを実行して出力された結果から探します。

bundle e rails routes -g comment

create のURLを指定すればOKです。

<%= form_with model: @comment, url: book_comments_path(@book), local: true do |form| %>
  <%= form.text_area :body %>
  <%= form.submit '作成' %>
<% end %>

次にコントローラを作成します。

bundle e rails g controller comments create destroy --no-helper --skip-routes

次にコントローラの処理を修正してコメントを作成する処理を追加します。
comments_controller.rb を修正していきます。
ストロングパラメータを追加します。

  private
  
  def comment_params
    params.require(:comment).permit(:body)
  end

comment を保存する処理を追加します。

  def create
    @book = Book.find(params[:book_id])
    @comment = @book.comments.new(comment_params.merge(user: current_user))
    if @comment.save
      redirect_to book_path(@book)
    else
      render 'books/show'
    end
  end

book と紐づける必要があるため、まずは book のデータを取得する必要があります。ルーティングを見てみると、URLから book の id が取得できることがわかります。ただし、id ではなく book_id となっている点に注意です。

rails routes を実行した結果出力されるURLを確認し、URLに含まれる id がどのように取得できるか確認する。その際、URLに含まれるものが :id となっていない場合、コントローラで該当の id を取得する際、params[:id] という書き方では id が取得できない。必ず、rails routes の出力結果と合わせるようにする。以下の場合は params[:book_id] と書くことで book の id が取得できる。

       Prefix Verb   URI Pattern                            Controller#Action
book_comments POST   /books/:book_id/comments(.:format)     comments#create

これでコメントを作成することができるようになったと思います。
空のままでは保存できないよう、バリデーションを追加しましょう。
models/comment.rb に以下のコードを追加します。

validates :body, presence: true

これでコメントが空のまま登録されることはなくなりました。

コメントを表示する

次に、登録したコメントを表示します。
今回は本の詳細ページに表示してみます。

コメント
<br />
<% @book.comments.each do |comment| %>
  <%= comment.body %><br />
<% end %>

簡単ですね。

コメントを削除する

次に、コメントを削除する処理を追加します。
まずはコメントを削除するリンクを追加しましょう。

<% @book.comments.each do |comment| %>
  <%= comment.body %>
  <%= link_to '削除', book_comment_path(@book, comment), method: :delete %><br />
<% end %>

URLヘルパーを使用する際は引数に注意しましょう。rails routes の出力結果を確認し、URLに : から始まる値が含まれている数の分だけ引数の数が必要となります。以下のコメントを削除するURLの場合は :book_id と :id がURLに含まれているため、2つの引数が必要となります。そのため、book_comment_path には引数を2つ(@book と comment)与えています。

book_comment DELETE /books/:book_id/comments/:id(.:format) comments#destroy

URLヘルパーを使用する際は引数に注意。URLに : から始まる値が含まれている数の分だけ引数の数が必要。

コントローラにコメントを削除する処理を追加します。

  def destroy
    book = Book.find(params[:book_id])
    book.comments.find(params[:id]).destroy
    redirect_to book_path(book)
  end

これで削除が完成しました。
ただし、今のままではコメントの削除が誰でもできてしまうため、コメントを行なった本人しか削除できないようにしたいと思います。
まずは削除のリンクを表示させないようにしてみます。

<% if comment.user == current_user %>
  <%= link_to '削除', book_comment_path(@book, comment), method: :delete %><br />
<% end %>

このように実装するとコメントを行なったユーザーとログインユーザーが同一の場合のみ削除することができます。ただし、実装の方法がイケてないという問題があります。そのため、以下のようなメソッドを使用するようにします。

<% if comment.user.me?(current_user.id) %>
  <%= link_to '削除', book_comment_path(@book, comment), method: :delete %><br />
<% end %>

me? というメソッドを使うようにしました。このメソッドは以下のように models/user.rb に作成することで使えるようになります。

def me?(user_id)
  id == user_id
end

テストは以下のようになります。

  describe '#me?' do
    subject { user.me?(user_id) }

    let(:user) { create(:user) }

    context 'レシーバと引数のID が同一の場合' do
      let(:user_id) { user.id }

      it { is_expected.to be true }
    end

    context 'レシーバと引数のID が同一ではない場合' do
      let(:other_user) { create(:user) }
      let(:user_id) { other_user.id }

      it { is_expected.to be false }
    end
  end

もちろんテストを実行するとグリーンになります。
これでコメント機能が完成しました。

この記事が気に入ったらサポートをしてみませんか?