背景
Twitterのようなアプリを作っていて、投稿に「いいね!」ボタンを付けようと思います。
作ろうとしているもののデータベース構造はこんな感じです。
この「いいね!」ボタンを押すと、ユーザーが投稿に「いいね!」でき、もう一度ボタンを押すと「いいね!」が解除できるようにしたいです。
モデル間のアソシエーションの設定は、過去にこの記事で行ってきました。
▼アソシエーションの設定
Railsで「いいね!」機能を作る - ①アソシエーションに別名をつける
今回は、controllerの記述を中心に紹介し、ボタンを押すと「いいね!」ができるところまでを目標にしてみたいと思います。なお、「いいね!」の解除機能は、この次の記事で実装します。
▼この記事の続編はこちら
Railsで「いいね!」機能を作る - ③「いいね!」を解除できるようにする
ゴール
ゴールは至ってシンプルで、users
とposts
の中間テーブルであるlikes
に適切なデータが入れば良いだけです。
入れるデータはログインしているユーザーのidと、「いいね!」ボタンを押した投稿のidです。
ただし、**ユーザーがすでに「いいね!」を押した投稿に対し、重複して「いいね!」を押すことはできません。**←この条件が結構難しくてハマりどころではないのかと思います。
とりあえず動かす① - 中間テーブルにデータを保存できるようにする
controller
とりあえず、中間テーブルにデータを保存するところからスタートしてみます。likes
テーブルにシンプルにuser_id
とpost_id
を保存するようにしてみました。
class LikesController < ApplicationController
def create
like = current_user.likes.new(post_id: clicked_post.id)
if like.save
flash[:success] = '投稿に「いいね!」しました。'
redirect_back(fallback_location: root_path)
else
flash[:alert] = '「いいね!」に失敗しました。'
redirect_back(fallback_location: root_path)
end
end
private
def clicked_post
Post.find(params[:post_id])
end
end
まず、privateメソッドでclicked_post
(ユーザーがクリックした投稿)を定義します。
そして、current_user.likes.new(post_id: clicked_post.id)
で、ボタンを押した投稿の「いいね!」をイニシャライズし、その次の行で保存しています。
redirect_back(fallback_location: root_path)
は「元のページに戻る」コードです。詳細はこちらの記事をご覧いただくのがわかり易いと思います。
view
ビューは、下図のように投稿のパーシャルの中に「いいね!」ボタンをつける想定です。私は以下のように書きました。
<!-- postのインスタンスは、パーシャルに渡されている前提です。 -->
<div>
<!-- 投稿者名・投稿日時・投稿内容 -->
<div>
<%= post.user.name %> <%= post.created_at %>
</div>
<div>
<%= post.content %>
</div>
<!-- いいね!ボタン -->
<div>
<%= form_with(model: @like, url: likes_path, local: true) do |f| %>
<%= f.hidden_field :post_id, value: post.id %>
<%= f.submit 'いいね!' %>
<% end %>
</div>
</div>
上記のコードでは、form_with
以下からLike
モデルに向かって、hidden_field
でpost_id
を送信しています。
これで、likes
テーブルに欲しいデータは入ります。ただ、入るには入るのですが…。Railsコンソールで確かめると下記のようになりました。
$ rails c
> user = User.first
> user.favorites
=> #<ActiveRecord::Associations::CollectionProxy
[#<Post id: 2, content: "テスト投稿です。", user_id: 2, created_at: "2020-08-27 12:36:45", updated_at: "2020-08-27 12:36:45">,
#<Post id: 2, content: "テスト投稿です。", user_id: 2, created_at: "2020-08-27 12:36:45", updated_at: "2020-08-27 12:36:45">]>
...同じユーザーが同じ投稿に対して持っている「いいね!」が2つになっています。。。これではいけませんね。。。
とりあえず動かす② - 同じ投稿に「いいね!」が2回できないようにする
そこで、ユーザーが「いいね!」をした投稿(favorites
)の中に、たった今、画面上で「いいね!」をされた投稿(clicked_post
)がないか探し、なければlikesテーブルにデータを保存するようにしたいと思います。
class LikesController < ApplicationController
def create
unless current_user.favorites.include?(clicked_post)
like = current_user.like.new(post_id: clicked_post.id)
if like.save
flash[:success] = '投稿に「いいね!」しました。'
redirect_back(fallback_location: root_path)
end
else
flash[:alert] = 'すでに「いいね!」しています。'
redirect_back(fallback_location: root_path)
end
end
private
def clicked_post
Post.find(params[:post_id])
end
end
unless current_user.favorites.include?(clicked_post)
の部分で、ユーザーのクリックした投稿が、ユーザーがこれまでに「いいね!」した投稿(favorites
)に含まれていないことを確認し、
その場合に、「いいね!」を保存できるようにcontrollerを書き換えました。favorites
の定義は、前回のこちらの記事を見てください。
これで、同じ投稿に2回「いいね!」できないようにするという条件はクリアしました。
...が、コードが汚いです
コントローラーに色々なものを書きすぎています。
コントローラーはできるだけ薄くするのがRailsによるアプリ作りの原則ですので、リファクタリング(コードを簡素化し、きれいにする)をしたいと思います。
リファクタリング
結論から言うと、リファクタリングの結果作成したのは、以下のコードです。
class LikesController < ApplicationController
def create
if current_user.like_this(clicked_post)
flash[:success] = '投稿に「いいね!」しました。'
redirect_back(fallback_location: root_path)
else
flash[:alert] = 'すでに「いいね!」しています'
redirect_back(fallback_location: root_path)
# 「いいね!」削除ボタンを実装すると、↑上記の2行は不要になります
end
end
private
def clicked_post
Post.find(params[:post_id])
end
end
class User < ApplicationRecord
has_many :likes
has_many :favorites, through: :likes, source: :post
def like_this(clicked_post)
self.likes.find_or_create_by(post: clicked_post)
end
end
まず、like_this
メソッドを定義します。ユーザーであるself
のlikes
内に、clicked_post
があるかどうかを探して、なければ作成します。
※ find_or_create_by
の仕様はこちらの記事がわかりやすかったです。
このメソッドで、コントローラーにゴニョゴニョあったif文は全て消えてしまいました。わー。。。。リファクタリング、大事です。
あとは、地味ですがこちらの部分もリファクタリングしました。
# これを
current_user.like.new(post_id: clicked_post.id)
# こうした
current_user.like.new(post: clicked_post)
*_id
同士を比べて、、、、という書き方は、基本的に全てインスタンス同士を比較する書き方に書き換えられそうです。
ここまでやったら、かなりRailsに詳しくなれそうです。。。。
次は、一度「いいね!」を押したボタンをクリックすると、「いいね!」を取りやめる、「いいね!」の削除機能について調べていきます。
▼こちらの記事です
Railsで「いいね!」機能を作る - ③「いいね!」を解除できるようにする
補足:マイグレーションを作るときに安全性をあげる
なお、順番が前後してしまいましたが、「同じ投稿に2回『いいね!』することを避ける」ために、DB側に以下のような設定もできます。
class CreateLikes < ActiveRecord::Migration[5.2]
def change
create_table :likes do |t|
t.references :user, foreign_key: true
t.references :post, foreign_key: true
t.timestamps
t.index [:user_id, :post_id], unique: true
end
end
end
t.index [:user_id, :post_id], unique: true
の部分で、重複したuser_id
とpost_id
のペアが登録されようとしたら、例外を発生させることができます。
同じ投稿に「いいね!」ができないようにする安全性を高めるためにも、やってみてもいいと思います^^
### 追記
2020.09.29 一部のコードをリファクタリングしました。