【Rails】いいね機能の作り方

プログラミングスクールの最終課題でオリジナルアプリを作成するにあたり、
いいね機能を実装したので、アウトプットしていきます。
Qiitaへの投稿に不慣れなため、稚拙な部分もありますがご容赦ください。

**前提条件**
・Rubyバージョン2.6.5
・Railsバージョン6.0.0
・「gem 'devise'によるUserモデル」と「投稿に関するPostモデル」は作成済みとします。

# いいね機能作成の流れ

下記の6ステップに分けていいね機能を実装していきます。

①いいね機能を管理する**「Favoriteモデル」**を作成する。
②各モデルに**アソシエーション**を記述する。
③URIパターンに**「:post_id」を含めたルーティング**を設定する。
（どの投稿に紐付くいいねか分かるようにするため、postsコントローラーのルーティングに
　ネストさせる）
④favoritesコントローラーでアクションを定義する。
⑤Postモデルに**「ログイン中のユーザーがその投稿に対していいねをしているか」**
　を判断するメソッドを定義する。
⑥投稿詳細のビューファイルに**いいねボタンを実装**する。

# ①いいね機能を管理するFavoriteモデルを作成する
まずは`rails g model`でFavoriteモデルを作成します。

```ターミナル
rails g model favorite
```

次に生成されたマイグレーションファイルにカラムを設定します。

この時アソシエーションの観点からいいね機能を考えると、
・一人のUserは複数の投稿にいいねできる
・一つのPostは複数のユーザーからのいいねを持つ

という**多対多**の関係性であることが分かります。

この場合、「Userのid」と「Postのid」を結び付ける中間テーブルを作成する必要があるため、
UserモデルとPostモデルそれぞれのidを外部キーとして設定します。

```db/migrate/*****_create_favorites.rb
class CreateFavorites < ActiveRecord::Migration[6.0]
  def change
    create_table :favorites do |t|
      t.references :user, null: false, foreign_key: true
      t.references :post, null: false, foreign_key: true
      t.timestamps
    end
  end
end
```
マイグレーションファイルを編集したため、変更をデータベースへ反映します。

```ターミナル
rails db:migrate
```

# ②各モデルにアソシエーションを記述する
```app/models/user.rb
class User < ApplicationRecord
  has_many :posts, dependent: :destroy
  has_many :favorites, dependent: :destroy
end
```

```app/models/post.rb
class Post < ApplicationRecord
  belongs_to :user
  has_many :favorites, dependent: :destroy
end
```
```app/models/favorite.rb
class Favorite < ApplicationRecord
  belongs_to :user
  belongs_to :post
end
```
`dependent: :destroy`は、親のレコードを削除した際に関連する子のレコードも全て削除するための記述です。
例えば「投稿Aを削除したのに、favoritesテーブルには投稿Aに関するいいねのレコードが残っている…」となるとテーブルの管理がしづらくなってしまいます。
これを解消するために`dependent: :destroy`を用いて、「投稿Aが削除されたら、投稿Aに関連するfavoritesテーブルのレコードを削除する」という設定をしておきます。

# ③URIパターンに「:post_id」を含めたルーティングを設定する

favoritesコントローラーのルーティングを設定します。

```config/routes.rb
resources :posts do
   resource :favorites, only: [:create, :destroy]
end
```

「**Userがいいねしたのは、どの投稿なのか**」を分かるようにするため、postsコントローラーのルーティングにネストさせます。
これにより、`rails routes`では下記のようになります。

```
post_favorites POST   /posts/:post_id/favorites(.:format)      favorites#create
post_favorite DELETE  /posts/:post_id/favorites/:id(.:format)  favorites#destroy
```
ルーティングに`:post_id`が含まれているのが分かります。
この`:post_id`が、「どの投稿なのか」を示す役割を果たします。

# ④favoritesコントローラーでアクションを定義する
次にfavoritesコントローラーを作成します。

```
rails g controller favorites
```

それではfavoritesコントローラーに、いいねボタンが押された時に対応するアクションを記述していきます。

**createアクション**

```app/controllers/favorites_controller
def create
  @post_favorite = Favorite.new(user_id: current_user.id, post_id: params[:post_id])
  @post_favorite.save
  redirect_to post_path(params[:post_id]) 
end
```

**ポイントの整理**
・`user_id`には、ログイン中のユーザーidを指定する。
・`post_id`には、ステップ③でルーティングに含めた`「:post_id」`を指定する。
→これにより、「**ログイン中のユーザーが、どの投稿にいいねしたのか**」が分かるようになる。
・saveメソッドでデータベースへ保存したのち、再度投稿詳細画面へリダイレクトする。

**destroyアクション**

```app/controllers/favorites_controller
def create
  @post_favorite = Favorite.find_by(user_id: current_user.id, post_id: params[:post_id])
  @post_favorite.destroy
  redirect_to post_path(params[:post_id]) 
end
```

**ポイントの整理**
・`user_id`には、ログイン中のユーザーidを指定する。
・`post_id`には、ステップ③でルーティングに含めた`「:post_id」`を指定する。
・destroyメソッドでデータベースのレコードを削除したのち、再度投稿詳細画面へリダイレクトする。

#補足：findとfind_byの違いについて
ここで`find`と`find_by`の違いを整理しておきます。
すでにご存知の方は読み飛ばしてください。

**findメソッド**
モデルの主キー(id)を引数に指定することで、該当するレコードを取得できるメソッド。
ここでいう「主キー(ID)」とは、レコードの一番左側にあるidのこと。
`Post.find(1)`や`Post.find(params[:id])`のように、**どのid(主キー)か分かっている場合**は、findメソッドを用いることができます。

**find_byメソッド**
モデルの主キー以外の条件でも、レコードを取得することができるメソッド。
（主キー(id)でも検索・取得可能）
今回の`user_id`や`post_id`のような**id(主キー)か不明で、別の条件でレコードを検索したい場合**はfind_byメソッドを用います。

# ⑤Postモデルに**「ログイン中のユーザーがその投稿に対していいねをしているか」**を判断するメソッドを定義する


```app/models/post.rb
def favorited?(user)
   favorites.where(user_id: user.id).exists?
end
```
いいねボタンがクリックされる度に、毎回呼び出すメソッドです。

**メソッド内の構成**
①`favorited?(user)`として、メソッドに引数を指定する。
　→次のステップで**`current_user`を指定**します。
②「その引数(current_user)のidと等しい`user_id`を持つレコードは、favoritesテーブル内に存在するか？」を`exists?`を用いて判断します。

これによりいいねボタンがクリックされた際、
・一致するレコードが存在しない＝「まだいいねしていない→`createアクション`へ」
・一致するレコードが存在する　＝「すでにいいね済み→`destroyアクション`へ」
と分岐させることができます。

# ⑥投稿詳細のビューファイルにいいねボタンを実装する

```app/views/posts/show.html.erb
<% if @post.favorite?(current_user) %>
    # 一致するレコードが存在する＝すでにいいね済みの場合はdestroyアクションへ
    <%= link_to post_favorite_path(@post.id), method: :delete do %>
        <span style="color:red;">❤︎</span>
    <% end %>
<% else %>
    # 一致するレコードが存在しない＝まだいいねしていない場合はcreateアクションへ
    <%= link_to post_favorites_path(@post.id), method: :post do %>
        <span>❤︎</span>
    <% end %>
<% end %>
```

投稿のインスタンス変数`@post`に対して、⑤で定義した`favorited?メソッド`を呼び出し、
引数にはログイン中のユーザー(current_user)を指定します。
これにより、メソッドの返り値が`trueの場合はdestroyアクション`、`falseの場合はcreateアクション`が実行されることになります。

**HTTPメソッドに関する注意点**
`link_toメソッド`のHTTPメソッドには、`GET`がデフォルトで指定されています。
一方`rails routes`を用いて、`createアクション`と`destroyアクション`のHTTPメソッドを確認してみると、それぞれ`POST`と`DELETE`であることが分かります。

```
post_favorites POST   /posts/:post_id/favorites(.:format)      favorites#create
post_favorite DELETE  /posts/:post_id/favorites/:id(.:format)  favorites#destroy
```
そのため`link_toメソッド`内で、`method: :post`と`method: :delete`としてHTTP通信の種類を指定する必要があります。


これでいいね機能の完成です！お疲れ様でした！
ご指摘等あれば、ご教授頂けますと幸いです。

ハートをFontAwesomeのアイコンにしたい方は、自分が書いたこちらの記事をご参照ください。
[【Rails】FontAwesomeの使い方／アイコンにアニメーションを付ける](https://qiita.com/RIN_HM/items/f1e467449ea41879ad49)

【参考にした記事】
[いいね機能の実装](https://qiita.com/hitochan/items/9db194ac915d362ea475)
[find, find_by, find_by_sqlメソッドの違いについて](https://qiita.com/nagao-koyanagi/items/d7b339c07c50a85ef5dc)