3
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

つぶやきアプリを作ってみよう⑪

Last updated at Posted at 2021-02-06

#コメント機能を実装しよう
ツイート詳細画面からツイートに対して、コメントが書き込めるようなコメント機能を実装しましょう。

コメントは、ツイートが必ず所有する情報ではないため、ツイートと別のテーブルで管理しなくてはなりません。
そのため、commentsテーブルを作る必要があります。
さらに、コメントはどのツイートに対してのコメントなのか、どのユーザーが投稿したコメントなのか判る必要があります。

そのため、userモデルとtweetモデルの2つにアソシエーションを組む必要があります。

まずはUserモデルとTweetモデルに加えて
コメントモデルを作成します。

% rails g model comment

マイグレーションファイルを編集しましょう。

db/migrate/XXXXXXXXXXXX_create_comments.rb
class CreateComments < ActiveRecord::Migration[6.0]
  def change
    create_table :comments do |t|
      t.integer :user_id
      t.integer :tweet_id
      t.text :text
      t.timestamps
    end
  end
end

ここで注意する点は、カラムにuser_idとtweet_idがあることです。

「誰が投稿したコメントなのか」がわかる必要があるので、
関連するユーザーのidを保存する必要があります。
そのidを保存するカラム名をuser_idとしました。

加えて「どのツイートに対してのコメントなのか」が判る必要があります。
関連するツイートのidを保存するカラムは、tweet_idとしました。

##マイグレーションを実行しましょう

% rails db:migrate

# 「ctrl + C」でローカルサーバーを終了

# 再度、ローカルサーバーを起動
% rails s

Sequel Proを開き、テーブルが追加されているか確認しましょう。

ここで、3つのテーブル同士の関係性を確認しましょう。
テーブルとテーブルをつなぐ線は、1対多の関係を表しています。
例としてtweetsテーブルからcommentsテーブルに伸びる線は、
一つのツイートが複数のコメントを持つ様を表しています。

****ER図添付
image.png

comment.rbを編集しましょう
コメントは、1人のユーザーと1つのツイートに所属するので、belongs_to :モデル単数形と記述することで、アソシエーションを定義します。

アソシエーションを定義しましょう。

###コメントモデルを編集

app/models/comment.rb

class Comment < ApplicationRecord
  belongs_to :tweet  # tweetsテーブルとのアソシエーション
  belongs_to :user  # usersテーブルとのアソシエーション
end

###ツイートモデルを編集

app/models/tweet.rb

class Tweet < ApplicationRecord
  validates :text, presence: true
  belongs_to :user
  has_many :comments, dependent: :destroy  # commentsテーブルとのアソシエーション
end

dependent: :destroyオプションを追加記述することで、
親モデルのTweetモデルが削除された際に、子モデルのCommentモデル(インスタンス)も削除されます。

###ユーザーモデルを編集

app/models/user.rb

class User < ApplicationRecord
  # Include default devise modules. Others available are:
  # :confirmable, :lockable, :timeoutable and :omniauthable
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :validatable
  has_many :tweets, dependent: :destroy
  has_many :comments, dependent: :destroy  # commentsテーブルとのアソシエーション
end

##コメントを投稿するためのルーティングを設定します。
コメントを投稿する際には、どのツイートに対するコメントなのかをパスから判断できるようにします。
ルーティングのネストという方法を使っていきます。
その一番の理由は、アソシエーション先のレコードのidをparamsに追加してコントローラーに送るためです。

【例】ルーティングのネスト

Rails.application.routes.draw do
  resources :親となるコントローラー do
    resources :子となるコントローラー
  end
end

##ルーティングを設定しましょう
今回は「コメント情報を作る機能」を実装するのでcreateアクションのルーティングとします。

config/routes.rb

Rails.application.routes.draw do
  devise_for :users
  root to: 'tweets#index'
  resources :tweets do
    resources :comments, only: :create
  end
  resources :users, only: :show
end

ルーティングを確認してみましょう。

Prefix Verb           URI Pattern                            Controller#Action
# 中略
tweet_comments POST   /tweets/:tweet_id/comments(.:format)   comments#create
# 中略

##コントローラーを作成しましょう。

% rails g controller comments
app/controllers/comments_controller.rb
class CommentsController < ApplicationController
  def create
    Comment.create(comment_params)
  end

  private
  def comment_params
    params.require(:comment).permit(:text).merge(user_id: current_user.id, tweet_id: params[:tweet_id])
  end
end

user_idカラムには、ログインしているユーザーのidとなるcurrent_user.idを保存し、
tweet_idカラムは、paramsで渡されるようにするので、params[:tweet_id]として保存しています。

続けて、コメントをしたらツイートの詳細画面に戻る処理をcomments_controller.rbに記述しましょう。redirect_toメソッドを使って記述します。

app/controllers/comments_controller.rb
class CommentsController < ApplicationController
  def create
    comment = Comment.create(comment_params)
    redirect_to "/tweets/#{comment.tweet.id}"  # コメントと結びつくツイートの詳細画面に遷移する
  end

  private
  def comment_params
    params.require(:comment).permit(:text).merge(user_id: current_user.id, tweet_id: params[:tweet_id])
  end
end

tweetsコントローラーのshowアクションを実行するには、ツイートのidが必要です。
そのため、ストロングパラメーターを用いた上で変数commentに代入します。

次は、コメントを投稿するためのフォームを作ります。
コメントはツイートの詳細画面から投稿したいので、
views/tweets/show.html.erbにフォームを記述します。

app/views/tweets/show.html.erb
<div class="contents row">
    <div class="content_post" >
      <p><%= @tweet.text %></p>
       <p><%= image_tag @tweet.image.variant(resize: '500x500'), class: 'tweet-image' if @tweet.image.attached?%></p>
      <span class="name">
        <a href="/users/<%= @tweet.user.id %>">
        <span>投稿者</span><%= @tweet.user.nickname %>
      </a>
      <% if user_signed_in? && current_user.id == @tweet.user_id %>
      </span>
      <%= link_to '編集', edit_tweet_path(@tweet.id), method: :get %>
      <%= link_to '削除', "/tweets/#{@tweet.id}", method: :delete %>
       <% end %>
    </div>

    <div class="container">
    <% if user_signed_in? %>
      <%= form_with(model: [@tweet, @comment], local: true) do |form| %>
        <%= form.text_area :text, placeholder: "コメントする", rows: "2" %>
        <%= form.submit "コメント" %>
      <% end %>
    <% else %>
      <strong><p>※※※ コメントの投稿には新規登録/ログインが必要です ※※※</p></strong>
    <% end %>
  </div>
</div>

ツイートの詳細画面でツイートと結びつくコメントを表示するためには、ビューを呼び出す前のコントローラーが実行されている時点で、まずはコメントのレコードをデータベースから取得する必要があるのでtweets_controller.rbのshowアクションを編集しましょう。

app/controllers/tweets_controller.rb
class TweetsController < ApplicationController
  before_action :set_tweet, only: [:edit, :show]
  before_action :move_to_index, except: [:index, :show]

  def index
    @tweets = Tweet.includes(:user).order("created_at DESC")
  end

  def new
    @tweet = Tweet.new
  end

  def create
    Tweet.create(tweet_params)
  end

  def destroy
    tweet = Tweet.find(params[:id])
    tweet.destroy
  end

  def edit
  end

  def update
    tweet = Tweet.find(params[:id])
    tweet.update(tweet_params)
  end

  def show
    @comment = Comment.new
    @comments = @tweet.comments.includes(:user)
  end

  private
  def tweet_params
    params.require(:tweet).permit(:image, :text).merge(user_id: current_user.id)
  end

  def set_tweet
    @tweet = Tweet.find(params[:id])
  end

  def move_to_index
    unless user_signed_in?
      redirect_to action: :index
    end
  end
end

tweets/show.html.erbでform_withを使用して、comments#createを実行するリクエストを飛ばしたいので、@comment = Comment.newというインスタンス変数を生成しておかないといけません。

tweetsテーブルとcommentsテーブルはアソシエーションが組まれているので、@tweet.commentsとすることで、@tweetへ投稿されたすべてのコメントを取得できます。

ビューでは誰のコメントか明らかにするため、
アソシエーションを使ってユーザーのレコードを取得する処理を繰り返します。
その際「N+1問題」が発生してしまうので、includesメソッドを使って解決しています

コメントを表示するためにツイート詳細ページを編集しましょう。

app/views/tweets/show.html.erb
<div class="contents row">
    <div class="content_post" >
      <p><%= @tweet.text %></p>
       <p><%= image_tag @tweet.image.variant(resize: '500x500'), class: 'tweet-image' if @tweet.image.attached?%></p>
      <span class="name">
        <a href="/users/<%= @tweet.user.id %>">
        <span>投稿者</span><%= @tweet.user.nickname %>
      </a>
      <% if user_signed_in? && current_user.id == @tweet.user_id %>
      </span>
      <%= link_to '編集', edit_tweet_path(@tweet.id), method: :get %>
      <%= link_to '削除', "/tweets/#{@tweet.id}", method: :delete %>
       <% end %>
    </div>

    <div class="container">
    <% if user_signed_in? %>
      <%= form_with(model: [@tweet, @comment], local: true) do |form| %>
        <%= form.text_area :text, placeholder: "コメントする", rows: "2" %>
        <%= form.submit "コメント" %>
      <% end %>
    <% else %>
      <strong><p>※※※ コメントの投稿には新規登録/ログインが必要です ※※※</p></strong>
    <% end %>
     <div class="comments">
      <h4><コメント一覧></h4>
      <% @comments.each do |comment| %>
        <p>
          <strong><%= link_to comment.user.nickname, "/users/#{comment.user_id}" %></strong>
          <%= comment.text %>
        </p>
      <% end %>
    </div>
  </div>
</div>

以上で完成です。

3
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?