ここではツイートに対してコメントができるようにする。
ツイートの投稿と似ているが、リソースのネストという考え方が出てくるので使いこなせるよう学習していく。
ツイートにコメントできるようにする
先ほどまではツイートの詳細画面を作ってきた。
次はこのツイート詳細画面からツイートに対してコメントができるようにする。
下記のようなものを作っていく。
【例】
コメントはツイートとは別のテーブルで管理しなくてはならない。
そのため、commentsテーブルを作る必要がある。
さらに、コメントはどのツイートに対してのコメントなのか、そして誰のコメントなのかが明示的でなくてはならない。
だとすると、userモデルとtweetモデルの2つとアソシエーションを組む必要がある。
色々と複雑そうに見えるが、テーブルを作り、ルーティング、コントローラ、ビューを作る流れはこれまでとほぼ同じである。
1点、ルーティングの仕方だけ少し違うので、そこだけ注意が必要である。
手順は以下のとおり。
作業内容
1.commentsテーブルを作る
2.ルーティングを記述する
3.フォームを作る
4.コントローラを作り、createアクションを記述する
5.ビューを編集する
1.commentsテーブルを作る
ツイートに対してのコメントをDBに保存するには、新しいテーブルとモデルを作らなければならない。
commentを保存するテーブルなのでcommentsテーブル、モデルはcommentモデルという名前になる。
テーブルの名前は複数形、モデル名は単数形になっていることに注意する。
① commentモデルを作る
以下のコマンドをターミナルで実行し、commentモデルを作成する。
ターミナル
$ rails g model comment
|
新規作成されるファイル
- app/models/comment.rb
- db/migrate/201XXXXXXXXXXXX_create_comments.rb
commentモデルファイルとcommentsテーブルのマイグレーションファイル(=設計図)が作成された。
② マイグレーションファイルを編集する
モデルを作った際にマイグレーションファイルができた。
次はこのマイグレーションファイルを編集して、commentsテーブルに必要なカラムの情報を追加していく。
先ほど作成したマイグレーションファイルを以下のように編集する。
db/migrate/201XXXXXXXXXXXX_create_comments.rb
class CreateComments < ActiveRecord::Migration[5.2]
def change
create_table :comments do |t|
t.integer :user_id
t.integer :tweet_id
t.text :text
t.timestamps
end
end
end
|
コメントの本文はtextカラムに保存していく。
ここで注意が必要なのは、カラムにuser_idとtweet_idがあることである。
コメントはまず誰が投稿したコメントなのか分かる必要があるため、結びつくユーザーのidを保存する必要がある。
コメントを投稿したユーザーのidを保存するカラムがuser_idとなる。
そしてもうひとつ、コメントはどのツイートに対してのコメントなのか明示する必要がある。
結びつくツイートのidを保存するカラムがtweet_idとなる。
③ マイグレートを実行する
マイグレーションファイルの記述は完成したので、次はrails db:migrateを実行してテーブルを作る。
以下のコマンドをターミナルで実行し、テーブルを作成する。
ターミナル
$ rails db:migrate
|
④アソシエーションを定義する
これによって、コメントに結びつくユーザーとツイートのidが保存できるカラムができた。
テーブルとテーブルをつなぐ棒線は一対多の関係を表している。
class Comment < ApplicationRecord belongs_to :tweet #tweetsテーブルとのアソシエーション belongs_to :user #usersテーブルとのアソシエーション end
コメントは一人のユーザーと一つのツイートに従属するためbelongs_to :モデル単数形を使う。
app/models/tweet.rb
class Tweet < ApplicationRecord belongs_to :user has_many :comments #commentsテーブルとのアソシエーション end
ツイートは複数のコメントを持つことができるため、has_many :モデル複数形を使ってアソシエーションを組む。
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 has_many :comments #commentsテーブルとのアソシエーション end
ユーザーは複数のコメントを投稿することができるため、has_many: モデル複数形を使ってアソシエーションを組む。
これで、3つのテーブルのアソシエーションを組むことができた。
2.ルーティングを記述する
commentsテーブルと、各モデルのアソシエーションは完成した。
次はコメントを投稿するためのルーティングを記述していく。
今回はルーティングを簡単に作成するためにresourcesメソッドを使って定義する。
resourcesメソッド
Railsの基本となるコントローラの7つのアクションで記載した7つのアクション名に対してのルーティングを自動で生成するメソッドである。
tweetsテーブルにとっての1ツイート、usersテーブルにとっての1ユーザー、つまり、テーブルに保存されるレコード1つの情報のことをリソースと呼ぶ。
以下の例のような記述では、bookというリソースに対して、基本となる7つのアクションをリクエストするルーティングが設定される。
【例】
config/routes.rb
Rails.application.routes.draw do resources :books end
Prefix Verb URI Pattern Controller#Action books GET /books(.:format) books#index POST /books(.:format) books#create new_book GET /books/new(.:format) books#new edit_book GET /books/:id/edit(.:format) books#edit book GET /books/:id(.:format) books#show PATCH /books/:id(.:format) books#update PUT /books/:id(.:format) books#update DELETE /books/:id(.:format) books#destroy
またcontrollerでbefore_actionの編集をした時と同様に、resourcesメソッドでもexceptやonlyオプションを組み合わせて特定のアクションへのルーティングのみを指定することができる。
コメントについてのルーティングを定義する前に、まずは今まで定義していたツイートとユーザーに関するルーティングをresourcesメソッドを使って書き換えて行く。
以下のようにルーティングをresourcesメソッドを使い書き換える。
config/routes.rb
Rails.application.routes.draw do devise_for :users root 'tweets#index' resources :tweets #tweets_controllerに対してのresourcesメソッド resources :users, only: [:show] #users_controllerに対してのresourcesメソッド end
tweetsコントローラは7つのアクション全て使っているが、usersコントローラはshowアクションしか使っていないため、onlyオプションでアクションの指定をする。
できたら、rake routesコマンドを使ってどのようにルーティングが定義されているか確認する。
今までのルーティングの記述とresourcesの関係
ここでは、今まで「get 'tweets' => 'tweets#index'」このような形式でルーティングを記述してきた。
わかりやすさを重視してこのように書いてきたが、resourcesを使用した方が少ないコードでタイプミスも起きにくいため、resourcesを使うことが一般的である。
以下のようにターミナルでrake routesコマンドを実行し、ルーティングを確認する。
ターミナル
$ rake routes
|
コマンドを実行して以下のようなルーティングが表示されれば成功。
ターミナル
Prefix Verb URI Pattern Controller#Action
root GET / tweets#index
tweets GET /tweets(.:format) tweets#index
POST /tweets(.:format) tweets#create
new_tweet GET /tweets/new(.:format) tweets#new
edit_tweet GET /tweets/:id/edit(.:format) tweets#edit
tweet GET /tweets/:id(.:format) tweets#show
PATCH /tweets/:id(.:format) tweets#update
PUT /tweets/:id(.:format) tweets#update
DELETE /tweets/:id(.:format) tweets#destroy
(省略)
|
次はcommentsコントローラのルーティングを記述する。
今まで通りに記述すると、下記のようなルーティングになる。
【例】
config/routes.rb
Rails.application.routes.draw do devise_for :users root 'tweets#index' resources :tweets resources :comments, only: [:create] resources :users, only: [:show] end
このルーティングによって発行されるパスは以下の通りである。
【例】
ターミナル
Prefix Verb URI Pattern Controller#Action
# 中略
comments POST /comments(.:format) comments#create
# 中略
|
ここで考えてみたいのだが、コメントには必ずコメント先というものが存在している。
今回の場合だと、コメント先のツイートがなければどのツイートに対してのコメントなのか分からなくなってしまう。
しかし今のままではパスの中にどのツイートのコメントなのかということを示す情報がない。
コメントを投稿する際には、どのツイートに対するコメントなのかをパスから判断できるようにしたいため、ここではネストという方法を使っていく。
ルーティングのネスト
ネストとはネストは「入れ子構造」を表すプログラミング用語である。
入れ子構造とは、マトリョーシカのように何かの中に何かが入っている状態を言う。
ルーティングのネストとは
Railsのルーティングもネストさせることができる。
つまり、1つのルーティングの中に、別のルーティングを入れ子にすることができるのである。
ルーティングのネストは少し複雑な話なので、最初にネストが必要な理由から確認する。
PicTweetでは、tweetというデータとcommentsというデータが親子関係であるためルーティングのネストを使用している。
データが親子関係にあるということは、上図のように1つのつぶやき(親)に対して複数のコメント(子)が関連づけられていることを指している。
そのため、コメントを追加するときは、必ず「idが何番のtweet」に関するコメントなのかを特定しなくてはならない。
そのために、ルーティングのネストを利用する。ネストを使うと、コントローラーで親のidを取得できるようになる。
つまり、親子関係にあるデータのうち、「子」に当たるデータを操作したいときは、「親」と「子」のルーティングをネストさせる必要があるということになる。
では、コメントの新規作成機能を追加する中で、使い方を詳しく見ていく。
以下のようにルーティングをネストさせながら記述する。
config/routes.rb
Rails.application.routes.draw do
devise_for :users
root 'tweets#index'
resources :tweets do
resources :comments, only: [:create]
end
resources :users, only: [:show]
end
|
doとendで挟むことで中の記述をネストさせることができる。
この記述によって、どのようなルーティングになるのだろうか?
ターミナルでrake routesコマンドを実行し、ルーティングを確認してみる。
以下のようにターミナルでrake routesコマンドを実行し、ルーティングを確認する。
ターミナル
$ rake routes
|
コマンドを実行して以下のようなルーティングが表示されれば成功。
ターミナル
URLに注目する。
ネストによって、/tweets/:tweet_id/comments(.:format)となっている。
リンクを飛ばすときは、この:tweet_idのところにコメントと結びつくツイートのidを記述する。
そうするとparamsのなかにtweet_idというキーが追加され、コントローラで扱うことができる。
少し複雑であるため、実際にフォームやアクションを作成しながら順を追って説明していく。
3.フォームを作る
先ほどまではresourcesメソッドやネスト構造などを使いながらcommentsコントローラのcreateアクションを定義していた。
次はコメントを投稿するためのフォームを作成する。
コメントはツイートの詳細画面から投稿できると便利なのでviews/tweets/show.html.erbにフォームを記述する。
以下のようにshow.html.erbを編集し、コメントフォームを作成する。
tweets/show.html.erb
<div class="contents row"> <div class="content_post" style="background-image: url(<%= @tweet.image %>);"> <% if user_signed_in? && current_user.id == @tweet.user_id %> <div class="more"> <span><%= image_tag 'arrow_top.png' %></span> <ul class="more_list"> <li> <%= link_to '編集', "/tweets/#{@tweet.id}/edit", method: :get %> </li> <li> <%= link_to '削除', "/tweets/#{@tweet.id}", method: :delete %> </li> </ul> </div> <% end %> <%= simple_format(@tweet.text) %> <span class="name"> <a href="/users/<%= @tweet.user.id %>"> <span>投稿者</span><%= @tweet.user.nickname %> </a> </span> </div> <div class="container"> <!-- ここからフォーム --> <% if current_user %> <%= form_tag("/tweets/#{@tweet.id}/comments", method: :post) do %> <textarea name="text" placeholder="コメントする" rows="2" cols="30"></textarea> <input type="submit" value="SENT"> <% end %> <% end %> </div> </div>
25行目でif current_userとすることで、ログインしていない状態では投稿フォームを出さないようにしている。
これは、ログインしていない状態ではコメントを投稿できないようにするためである。
また、form_tagのurlのところに注目。
tweets/show.html.erb
<%= form_tag("/tweets/#{@tweet.id}/comments", method: :post) do %>
|
HTTPメソッドがpostメソッドになっている。これはツイートのフォームを作った際と同じである。
ここで重要なところはform_tagの後に続くかっこの中のurlである。
このurlの中に式展開させながら@tweet.idを記述することによって、paramsの中にコメントと結びつくツイートのidが追加される。
なぜこのようなことをする必要があるのか、その理由はコントローラを作るときに説明する。
4.コントローラを作り、createアクションを記述する
次はコントローラとアクションを作成する。
まずはcommentsコントローラを作る必要がある。rails gコマンドでcommentsコントローラを作成する。
以下のコマンドをターミナルで実行し、commentsコントローラを作成する。
ターミナル
$ rails g controller comments
|
次はcommentsコントローラ内でcreateアクションを作る。createアクションは以下の手順で進めていく。
① createアクションを定義する
comments_controller.rb
まずはcommentsコントローラにcreateアクションを定義する。
class CommentsController < ApplicationController def create Comment.create(text: params[:text], tweet_id: params[:tweet_id], user_id: current_user.id) end end
user_idカラムにはcurrent_user.id(=ログインしているユーザーのid)を保存している。
② ストロングパラメーターを指定する
しかしストロングパラメーターを指定していないので、不正な情報を受け取ってしまう可能性がある。
comments_controller.rb
privateメソッドを使って、comment_paramsメソッドをコントローラ以外のところで呼び出せないようにしていて、permitメソッドで受け取るparamsのキーを指定している。
③ リダイレクト先をtweets/show.html.erbにする
redirect_toメソッドを使って画面遷移の記述をしていく。
comments_controller.rb
class CommentsController < ApplicationController def create @comment = Comment.create(text: comment_params[:text], tweet_id: comment_params[:tweet_id], user_id: current_user.id) redirect_to "/tweets/#{@comment.tweet.id}" #コメントと結びつくツイートの詳細画面に遷移する end private def comment_params params.permit(:text, :tweet_id) end end
tweetsコントローラのshowアクションを実行するにはツイートのidが必要である。
ここではアソシエーションを使って、@commentと結びつくツイートのidを記述していく。
5.ビューを編集する
① コメントをビューに表示させる
コメントを表示する場所はツイートの詳細画面とする。このようにするとコメントを投稿した後に、先ほど記述したredirect_toメソッドによってツイートの詳細画面に遷移してコメントの確認ができる。
なのでまずはtweetsコントローラのshowアクションを編集する。
tweets_controller.rb
表示させたいツイートのレコード@tweetは既に取得できているので、ツイートと結びつくコメントのレコードをデータベースから取得する。
tweetsテーブルとcommentsテーブルはアソシエーションが組まれているため、@tweet.commentsとすることで、@tweetについて投稿された全てのコメントのレコードを取得することができる。
また、ビューでは誰のコメントかを明らかにするために、アソシエーションを使ってユーザーのレコードを取得する処理をする。
その時にN + 1問題が発生してしまうため、includesメソッドを使って、N + 1問題を解決している点に注意。
以下のようにtweets/show.html.erbを編集する
コントローラで、あるツイートについて投稿された全てのコメントのレコードを取得することができたので、それをビューで表示する。
@commentsには複数のコメントのレコードが入っているので配列の形をとっている。
なのでビューに表示させるためにはeachメソッドを使って、ひとつひとつのレコードに分解してから表示させる。
さらに、コメントをしたユーザー名をクリックしたらそのユーザーページに遷移するようにしたいので、名前のところにlink_toメソッドを使ってリンクを作る(リンクはまだクリックしない)。
tweets/show.html.erb
# 中略 <div class="container"> <% if current_user %> <%= form_tag("/tweets/#{@tweet.id}/comments", method: :post) do %> <textarea name="text" placeholder="コメントする" rows="2" cols="30"></textarea> <input type="submit" value="SENT"> <% end %> <% end %> <!-- ここから追記してください --> <div class="comments"> <h4><コメント一覧></h4> <% if @comments %> <% @comments.each do |comment| %> <p> <strong><%= link_to comment.user.nickname, "/users/#{comment.user_id}" %>:</strong> <%= comment.text %> </p> <% end %> <% end %> </div> </div> </div>
こちらではif @commentsとすることで、もし@commentsが空だった場合でもエラーが起こらないようにしている。
eachメソッドで繰り返しコメントを表示します。名前のところはlink_toメソッドを使ってリンクを作る。
ユーザーのidはcomment.user_idと記述することで、コメントを投稿したユーザーのidをparamsとして送る。
16行目で「comment.user」と記述し、コメントしたユーザーのレコードを取得しているが、includesメソッドで既にデータベースからレコードを取得しているのでSQL文は発行されず、N + 1問題は発生しない。
② cssファイルを編集する
コメントの一覧画面を綺麗に表示させるために、cssファイルを編集する。ここではrailsの実装の学習を目的としていますため、css等の説明は割愛する。
以下のようにassets/stylesheets/style.scssを編集する
style.scssの318行目以降のコードを全て削除して、以下のコードをコピー&ペーストをして編集する。
style.scss
.success { @include boxBase(20px 0 ,30px); box-shadow: 0 0 10px rgba($black,0.2); background-color: $white; box-sizing: border-box; text-align: center; h3 { @include baseMargin; } } } form { h3 { @include baseMargin(0 0 20px); text-align: center; font-weight: normal; font-size: 20px; color: $dark; } input,textarea { width: 100%; @include boxBase(5px 0 15px,10px); border: 1px solid $gray; border-radius: 5px; font-family: "游ゴシック", "YuGothic"; } input[type="submit"] { @extend .transition; background-color: $accent; border-radius: 20px; color: #fff; border: 0; font-size: 18px; &:hover { background-color: lighten($accent, 10%); } } } .container { @include boxBase(20px 0 ,30px); box-shadow: 0 0 10px rgba($black,0.2); background-color: $white; box-sizing: border-box; } .comments { padding: 5px; margin-top: 15px; a { color: $accent; &:hover { text-decoration: underline; } } } footer { @include boxBase; color: $gray; p { text-align: center; } }
以下のようにtweets/new.html.erbを編集する
少しcssのクラスを編集したため、他のビューファイルも編集する必要がある。
以下のようにtweets/new.html.erbを編集する。
tweets/new.html.erb
<div class="contents row"> <div class="container"> <%= form_tag('/tweets', method: :post) do %> <h3> 投稿する </h3> <input type="text" name="image" placeholder="Image Url"> <textarea name="text" placeholder="text" rows="10" cols="30"></textarea> <input type="submit" value="SENT"> <% end %> </div> </div>
③ マイページのビューを編集する
今、users_controller.rbは以下のようになっている。
users_controller.rb
class UsersController < ApplicationController def show @nickname = current_user.nickname @tweets = current_user.tweets.page(params[:page]).per(5).order("created_at DESC") end end
ここユーザーページは自分自身のツイートしか見られない仕様になっていたので、current_userを使うことでログインしているユーザー、即ち自分自身のツイートを見ることが出来た。
しかし、今はツイートの詳細ページからコメントをしたユーザーのページに遷移出来るような仕様になっている。
そのためcurrent_userを使ったままでは、どのユーザー名をクリックしても自分自身のユーザーページしか表示されない(実際に試してみると、そのようになっているはずである)。
クリックしたユーザーのページに遷移できるように、users_controllerのshowアクション編集する。
users_controller.rb
class UsersController < ApplicationController def show user = User.find(params[:id]) @nickname = user.nickname @tweets = user.tweets.page(params[:page]).per(5).order("created_at DESC") end end
3行目では、コメント欄に表示されるユーザーをクリックすることで送られたidをparamsで取得して、そのユーザーのレコードをuserという変数に代入している。
5 行目ではアソシエーションを利用することで、そのユーザーが投稿したツイートを取得して、@tweetsに代入している。