【Rails】いいね機能の実装

現在絶賛作成中のポートフォリオでいいね機能を実装する際、色々躓いてしまったので反省もかねてメモしとこうと思います。

#環境
- Rails 5.2.2
- mysql 8.0.17
- macOS

#実現したいこと
Twitterのようないいね機能の実装。

##そのために必要な処理
いいねボタンを押下した場合、

- 「いいねしたユーザーのid：user_id」と「いいねされた投稿のid：post_id」が「中間テーブル：likesテーブル」に保存される。
- いいねボタンが「いいね追加」と「いいね削除」で切り替わる。

##必要手順
1. 中間テーブルの作成
2. テーブルの紐付け
3. ルーティングの設定
4. コントローラー側の処理
5. ビューでの表示処理

大体こんな感じです。

では順番に見ていきましょう。

#中間テーブルの作成
まずは、「いいねしたユーザー」と「いいねされた投稿」を保存するテーブルの作成からです。

```
$ rails g model Like user:references post:references
```

referencesを指定することで、外部キー制約が自動で付きます。
非常に便利。

問題なければこのまま

```
$ rails db:migrate
```

しちゃいましょう。

#テーブルの紐付け
お次はモデルでhas_manyを使ってテーブルを紐付けます。

これに関しては、
[【Rails】投稿とユーザーの紐付け（一対多）のメモ](https://qiita.com/soehina/items/7238ec11d5587fdeed19)
でも似たような記事を書きましたので、復習になります。

```post.rb
has_many :likes
has_many :users, through: :likes

def liked_by?(user)
  likes.where(user_id: user.id).exists?
end
```

いいねされた投稿に対して、いいねしたユーザーは複数いるのでhas_manyを使います。

ここで使われているthroughは、likeテーブルを経由して直接userテーブルと繋げるためのものです。

また、

```post.rb
def liked_by?(user)
  likes.where(user_id: user.id).exists?
end
```
この子は「すでにいいねしたかどうか」を判断するためのメソッドで、後にビュー側で必要となってきます。

一応簡単に説明すると、
**likesテーブルの「いいねしたユーザー：user_id」カラムにuser.idが存在するのか探す**という処理です。

引数userにはビューでcurrent_userを指定して入れます。

```user.rb
has_many :likes, dependent: :destroy
has_many :like_posts, through: :likes, source: :post
```

ユーザー側でもしてることはほとんど変わりません。
一点気をつけるとすれば、

```user.rb
has_many :like_posts, through: :likes, source: :post
```
の部分です。

多くの関連記事でlike_postsのところがpostsになっていたのですが、もし他に has_many :posts があるなら名前が被らないようにしましょう。

railsが混乱してエラーになってしまうので、like_postsのように名前が被らないように注意することをおすすめします。

#ルーティングの設定
お次はルーティングです。

```routes.rb
resources :posts do
  post 'add' => 'likes#create'
  delete '/add' => 'likes#destroy'
end
```
postのidをとってくるためにネストします。
中は普通にルーティング設定してあげればおkです。

#コントローラー側の処理
ここまできたらあともう少し。

処理を書くためにコントローラーを作成します。

```
$ rails generate controller likes
```
コントローラーができたら以下の処理を記述します。

```likes_controller.rb
class LikesController < ApplicationController
    before_action :authenticate_user!
    before_action :set_like

    def create
        user = current_user
        post = Post.find(params[:post_id])
        like = Like.create(user_id: user.id, post_id: post.id)
    end
    def destroy
        user = current_user
        post = Post.find(params[:post_id])
        like = Like.find_by(user_id: user.id, post_id: post.id)
        like.delete
    end

    private
    def set_like
        @post = Post.find(params[:post_id])
    end
end
```

create/destroyアクションで共通する処理は以下になります。

```likes_controller.rb
user = current_user
post = Post.find(params[:post_id])
```

- いいねするユーザーであるcurrent_userを変数userに格納
- いいねされた投稿のidとPostテーブルのidが一致するものをfindで見つけて変数postに格納

##createアクション
createアクションでは、いいねされた場合の処理を記述します。

```likes_controller.rb
like = Like.create(user_id: user.id, post_id: post.id)
```

Likeテーブルに、
**user_idが、先ほどcurrent_userを格納した変数userのidで、post_idが、いいねされたPostテーブルのidを格納した変数post**
のデータをcreateで作成する処理です。

##destroyアクション
destroyアクションでは、いいねが取り消された場合の処理を記述します。

```likes_controller.rb
like = Like.find_by(user_id: user.id, post_id: post.id)
like.delete
```
find_byで、**user_idがcurrent_userのidと一致するもの且つ、post_idがいいねされたpostのidと一致するものを探して、変数likeに格納**します。

そしてdeleteメソッドでlikeを削除。

##set_post
地味に一番下のところに、

```likes_controller.rb
private
def set_like
    @post = Post.find(params[:post_id])
end
```

とありますが、こちらはjsのところで必要となってきますのでひとまずおいといてください。

#ビュー側の処理
index.html.erbでは投稿一覧を表示しています。
この投稿一つ一つにいいねぼたんを表示させたいわけですが、そのためにはいくつか階層を分ける必要があります。

とりあえず、いいねボタンを表示したいところに以下のように記述しましょう。

```erb:index.html.erb
<div id="like-btn-<%= post.id %>">
   <%= render 'likes/like', post: post %>
</div>
```
renderを使用することで、いいねだけを表示するviewを表示します。

post: post は、**いいねだけを表示するviewのpostにpost(ここでは@postsをeachで回してます)の情報を入れるで**というくらいの意味あいだと理解して問題ないかと。

例えば、postの情報とは違った情報も渡したいのであれば、それに適応したものを渡してあげたらおk（私の場合はブックマーク一覧のviewでも同じようにいいねできるようにしたかったので、別にviewを作成してブックマークされている投稿の情報を格納したlikeを post: like として設定していました）。

##_like.html.erb
「いいねだけを表示するview」である_like.html.erbを作成します。

```erb:app/views/likes/_like.html.erb
<% if post.liked_by?(current_user) %>
   <%= link_to(post_add_path(post), method: :delete, remote: true, id: :"like-button-#{post.id}") do %>
       <i class="fa-lg fas fa-heart icon-btn liked"></i>
   <% end %>
<% else %>
   <%= link_to(post_add_path(post), method: :post, remote: true, id: :"like-button-#{post.id}") do %>
       <i class="fa-lg fas fa-heart icon-btn not-like"></i>
   <% end %>
<% end %>
```

###if post.liked_by?(current_user)
まずこのif文は、先ほどmodelで定めたメソッドを使ってすでに現在のユーザーがいいねしてるかどうかを判定します。

- すでにいいねしてる場合  → link_toはmethod: :delete
- まだいいねしてない場合  → link_toはmethod: :post 

となるわけですね。

###remote: true
link_toに記述しているremote: trueですが、この子はコントローラーに値を送信する役割を果たし、ajaxを発火してくれる優秀な子です。
ajaxを利用することで、画面全体をリダイレクトする必要がなく、いいねアイコンの部分のみ更新してくれるので、アプリ全体の負担が減ります。

では最後に、remote: trueで呼び出すためのjsファイルを作ってあげましょう。

##create.js.erb/destroy.js.erb
_like.html.erbと同階層に次の二つのファイルを作成します。

```erb:create.js.erb
$('#like-btn-<%= @post.id %>').html("<%= escape_javascript(render partial: "likes/like", locals: { post: @post }) %>");
```

```erb:destroy.js.erb
$('#like-btn-<%= @post.id %>').html("<%= escape_javascript(render partial: "likes/like", locals: { post: @post }) %>");
```

内容はどっちも一緒です。

先ほどコントローラー側で設定したset_likeで、いいねされた投稿のidを取得して、それをいいねアイコンのidとしています。

ここまでできたら、いいねボタンの完成です！

あとはいいねしたときと外した時でスタイルを変更するなど、創意工夫してください。

#まとめ
いいね機能はほとんどのアプリで使うと思うので、結構情報があったのですが、いまいち仕組みが理解ができなくて結構苦戦しました。
特に中間テーブルへの保存で時間を取られたと思います。

できる範囲で細かく書いたので、誰かの助けになれば幸いです。

ではでは。