次はユーザーマイページを実装していきましょう。
showアクションを使用して実装していきます。
誰がどのつぶやきをしたか判断するためにつぶやきにuser_idを付与します。
##tweetsテーブルにuser_idカラムをinteger型で追加
% rails g migration AddUserIdToTweets user_id:integer
% rails db:migrate
% rails s #サーバー再起動
ツイートを投稿したユーザーとは現在ログインしているユーザーのことです。
tweetsテーブルのuser_idカラムにcurrent_userのidを保存します。
##current_userメソッド
device導入後使用可能なメソッドで現在ログインしているユーザーの情報を取得できます。
ログイン中のユーザーIDをツイート共にツイートを保存したいので
2つのハッシュを統合するときに使うmergeメソッドを利用して保存します。
##mergeメソッド
ハッシュを結合させるときに使用するメソッドです。
tweet = { name: "スズキ", text: "おはよう!", image: "sun.jpeg" }
id = { user_id: "1" }
tweet.merge(id)
=> {:name=>"スズキ", :text=>"おはよう!", :image=>"sun.jpeg", :user_id=>"1"}
class TweetsController < ApplicationController
before_action :set_tweet, only: [:edit, :show]
before_action :move_to_index, except: [:index, :show]
def index
@tweets = Tweet.all
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
end
private
def tweet_params
params.require(:tweet).permit(:name, :image, :text).merge(user_id: current_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
##アソシエーション
今回のように、ツイートを行なったユーザーの情報を取得するなど、
複数のモデルから情報を必要とするケースがあります。
テーブル同士で関連付けてお互いのモデルにアクセスできるようへする
アソシエーションというものがあります。
##has_manyメソッド
Userモデルの視点で考えると
1人のユーザーは複数の投稿を所有しています。
この状態のことをhas manyの関係といいます。
今回の場合は「User has many Tweets」の状態です。
他のモデルとの間に「1対多」のつながりがあることを示すのがhas_manyメソッドです。
Userモデルを編集しましょう。
class User < ApplicationRecord
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :validatable
has_many :tweets
end
##belongs_toメソッド
1つの投稿は、1人のユーザーが投稿したもので投稿は必ず1人のユーザーに所属します。
この状態を belongs toの関係といい、今回の場合は「Tweet belongs to User」の状態です。
Tweetモデルと他のモデル(User)との間に「1対1」のつながりがあることを示すのがbelongs_toメソッドです。
Tweetモデルを編集しましょう。
class Tweet < ApplicationRecord
validates :text, presence: true
belongs_to :user
end
アソシエーションでbelongs_toを指定した場合は、
相手のモデルのid(今回はuser_id)が存在するというバリデーションは不要です。
ルーティングを設定していきましょう。
Rails.application.routes.draw do
devise_for :users
# For details on the DSL available within this file, see https://guides.rubyonrails.org/routing.html
root to: 'tweets#index'
resources :tweets
resources :users, only: :show
end
/users/:idのパスでリクエストした際にusers_controller.rbのshowアクションを実行するルーティングが設定できました。
ビューにリンクを追加しましょう
<!DOCTYPE html>
<html>
<head>
<title>Tsubuyaki</title>
<%= csrf_meta_tags %>
<%= csp_meta_tag %>
<%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %>
<%= javascript_pack_tag 'application', 'data-turbolinks-track': 'reload' %>
</head>
<body>
<header class="header">
<div class="header__bar row">
<h1 class="grid-6"><a href="/">Tsubuyaki</a></h1>
<div class="top_contents">
<div class="user_nav grid-6">
<% if user_signed_in? %>
<div class="user_nav grid-6">
<%= link_to "マイページ", "/users/#{current_user.id}", class: "post" %>
<%= link_to "ログアウト", destroy_user_session_path, method: :delete,class: "post" %>
<%= link_to "投稿する", new_tweet_path, class: "post" %>
</div>
<% else %>
<div class="grid-6">
<%= link_to "ログイン", new_user_session_path, class: "post" %>
<%= link_to "新規登録", new_user_registration_path, class: "post" %>
</div>
<% end %>
</div>
</div>
</header>
<%= yield %>
<footer>
<p>
Copyright Tsubuyaki 2021.
</p>
</footer>
</body>
</html>
##コントローラーを作成しましょう
users_controller.rbを作成します。
% rails g controller users
showアクションで表示するのに必要な情報は「ニックネーム」と「ログイン中のユーザーのツイート投稿」です。
それぞれを@nicknameと@tweetsというインスタンス変数に代入します。
class UsersController < ApplicationController
def show
@nickname = current_user.nickname
@tweets = current_user.tweets
end
end
@nicknameはcurrent_userを利用し、
現在ログインしているユーザーが持つnicknameカラムの値を取得しています。
@tweetsも現在ログインしているユーザーのツイート投稿を取得して、
インスタンス変数に代入しています。
##マイページのビューを作成
app/views/usersディレクトリの中にshow.html.erbというファイルを作成します。
<div class="contents row">
<p><%= @nickname %>さんの投稿一覧</p>
<% @tweets.each do |tweet| %>
<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"><%= tweet.name %></span>
</div>
<% end %>
</div>
Tweetモデルに対して「Tweet belongs to User」という形でアソシエーションを定義しているので、Tweetモデルのインスタンスが入った変数.userと記述すると、インスタンスが属しているUserモデルのインスタンスを取得できます。
コンソールを利用して例を見てみましょう。
【例】アソシエーションを利用しない
% rails c
[1] pry(main)> tweet = Tweet.find(1)
[2] pry(main)> User.find(tweet.user_id)
=> #<User id: 1, email: "test@gmail.com", encrypted_password: "@@@@@@@@@@@@@@@@@", reset_password_token: nil, reset_password_sent_at: nil, remember_created_at: nil, sign_in_count: 1, current_sign_in_at: "2014-12-06 09:00:00", last_sign_in_at: "2014-12-06 09:00:00", current_sign_in_ip: "127.0.0.1", last_sign_in_ip: "127.0.0.1", created_at: "2014-12-06 09:00:00", updated_at: "2014-12-06 09:00:00", nickname: "test_ruby">
アソシエーションを利用すると、以下のように簡潔なコードで記述できます。
【例】アソシエーションを利用する
% rails c
[1] pry(main)> tweet = Tweet.find(1)
[2] pry(main)> tweet.user
=> #<User id: 1, email: "test@gmail.com", encrypted_password: "@@@@@@@@@@@@@@@@@", reset_password_token: nil, reset_password_sent_at: nil, remember_created_at: nil, sign_in_count: 1, current_sign_in_at: "2014-12-06 09:00:00", last_sign_in_at: "2014-12-06 09:00:00", current_sign_in_ip: "127.0.0.1", last_sign_in_ip: "127.0.0.1", created_at: "2014-12-06 09:00:00", updated_at: "2014-12-06 09:00:00", nickname: "test_ruby">
このようにスッキリしたコードが書けるので、これを利用して表示させてみましょう。
しかしここでトップページに行こうとするとエラーが起きます
ツイートを投稿した際、user_idも一緒に保存するようにしましたが、
それ以前に投稿したツイートはuser_idがNULLのままになっているためです。
sequel proで確認してユーザーIDを入力してみましょう。
NULLとなっているところに存在しているユーザーIDの数字を入れてください。
エラーが解消されるはずです。
他の投稿者のマイページに行けるようにビューを編集しましょう。
<div class="contents row">
<% @tweets.each do |tweet| %>
<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>
</span>
<%= link_to '詳細', tweet_path(tweet.id), method: :get %>
<%= link_to '編集', edit_tweet_path(tweet.id), method: :get %>
<%= link_to '削除', "/tweets/#{tweet.id}", method: :delete %>
</div>
<% end %>
</div>
しかしこのままだとどの投稿の投稿者名をクリックしてもログインユーザーの詳細ページに移動してしまいます。
この問題は後ほど解決します。
###tweets/show.html.erbを編集
ツイート詳細画面の投稿者の部分も@tweet.nameとnameを使う文になっているため、
アソシエーションを利用する形に変更しましょう。
マイページに飛ぶリンクも設置します。
<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>
</span>
<%= link_to '編集', edit_tweet_path(@tweet.id), method: :get %>
<%= link_to '削除', "/tweets/#{@tweet.id}", method: :delete %>
</div>
</div>
##N+1問題
ここで一つ問題が起こっています。ツイートからユーザーの情報を呼び出す際にN+1問題が発生しています。
N+1問題とは、アソシエーションを利用した場合に限り、データベースへのアクセス回数が多くなってしまう問題です。
今回の場合tweetsに関連するusersの情報の取得に、ツイート数と同じ回数のアクセスが必要になります。
これはアプリケーションのパフォーマンス低下につながります。
includesメソッドを利用して解決します。
##includesメソッド
includesメソッドは、引数に指定された関連モデルを1度のアクセスでまとめて取得できます。
書き方は、includes(:紐付くモデル名)とします。引数に関連モデルをシンボルで指定します。
モデル名.includes(:紐付くモデル名)
コントローラーを編集します。
class TweetsController < ApplicationController
before_action :set_tweet, only: [:edit, :show]
before_action :move_to_index, except: [:index, :show]
def index
@tweets = Tweet.includes(:user)
end
#中略
end
includesメソッドを使用するとすべてのレコードを取得するため、allメソッドは省略可能です。
##投稿画面のビューを変更
ツイートを表示する際にアソシエーションを利用して投稿者のニックネームが表示されるようになったので、
nameカラムは必要なくなり、投稿時に「Nickname」の値を入力する必要もなくなりました。
なので削除していきましょう。
rb:app/views/tweets/new.html.erb
<div class="contents_form">
<div class="container_box">
<h3>投稿する</h3>
<%= form_with(model: @tweet, local: true) do |form| %>
<%= form.text_field :name, placeholder: "ニックネーム", class: 'container'%> #この行を削除
<%= form.text_area :text, placeholder: "text", rows: "10", class: 'container'%>
<%= form.file_field :image %>
<%= form.submit "つぶやく", class: 'container'%>
<% end %>
</div>
</div>
編集画面のビューも同様に削除していきます。
nameカラムはもう使用しないので、
ツイートの保存時にnameカラムへ情報を保存しないようにストロングパラメーターに変更を行います。
class TweetsController < ApplicationController
before_action :set_tweet, only: [:edit, :show]
before_action :move_to_index, except: [:index, :show]
def index
@tweets = Tweet.includes(:user)
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
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
:nameカラムを削除しました。
テーブルからカラムを削除するためのマイグレーションを作成して、カラムの削除を実行しましょう。
% rails g migration Removeカラム名From削除元テーブル名 削除するカラム名:型
# マイグレーションの作成
% rails g migration RemoveNameFromTweets name:string
# マイグレーションの実行
% rails db:migrate
# 「control + C」でローカルサーバーを停止
# ローカルサーバーを起動
% rails s
削除されたnameカラムをtweet.nameと記述し利用している文は、1行まるごと削除します。
<div class="contents row">
<p><%= @nickname %>さんの投稿一覧</p>
<% @tweets.each do |tweet| %>
<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"><%= tweet.name %></span> #この行を削除
</div>
<% end %>
</div>
さて、ここで以前置き去りにしていた問題を解決しましょう。
投稿者名をクリックするとログインユーザーのページに遷移してしまうという問題です。
コントローラーでインスタンス変数を変更しましょう。
クリックされたユーザーのidから情報を取得して、ビューに受け渡します。
class UsersController < ApplicationController
def show
user = User.find(params[:id])
@nickname = user.nickname
@tweets = user.tweets
end
end
このままだと投稿に対して全てのユーザーに編集と削除ボタンが表示されてしまっています。
条件を追加して、投稿したユーザーだけに表示されるようにしましょう。
current_user.id == tweet.user_idと記述することで、
「現在ログインしているユーザー」と「ツイートを投稿したユーザー」が同じか否かを判定することができます。
これにif文を組み合わせることで表現できます。
ビューを編集していきましょう。
<div class="contents row">
<% @tweets.each do |tweet| %>
<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>
</span>
<%= link_to '詳細', tweet_path(tweet.id), method: :get %>
<% if user_signed_in? && current_user.id == tweet.user_id %>
<%= link_to '編集', edit_tweet_path(tweet.id), method: :get %>
<%= link_to '削除', "/tweets/#{tweet.id}", method: :delete %>
<% end %>
</div>
<% end %>
</div>
<div class="contents row">
<p><%= @nickname %>さんの投稿一覧</p>
<% @tweets.each do |tweet| %>
<div class="content_post" >
<p><%= tweet.text %></p>
<p><%= image_tag tweet.image.variant(resize: '500x500'), class: 'tweet-image' if tweet.image.attached?%></p>
</div>
<% end %>
</div>
以上です。