【Rails】お気に入り機能を実装しよう！

今回もレシピアプリを例にお気に入り機能を実装していきます。

#####完成イメージ
![a2b9460dbe7757921f96c6dd75b8b6cb.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1140333/68e14d58-e942-550e-d8c9-8851a272703e.gif)

##お気に入り機能の実装
####モデルの作成
お気に入りに必要な情報は以下の通りになります。
**・どの投稿をお気に入りしたかの情報（レシピ情報）**
**・誰がお気に入りしたかの情報（ユーザ情報）**

この２つの情報を保存するためにlikesテーブルを作成します。

```:ターミナル
rails g model likes
```
上記のコマンドで作成されたマイグレーションファイルを以下のように編集します。
references型でuserとrecipeに紐付けます。

```ruby:db/migrate/2021xxxxxxxxxx_create_likes.rb
class CreateLikes < ActiveRecord::Migration[6.0]
  def change
    create_table :likes do |t|
      t.references :user,   foreign_key: true  #追記
      t.references :recipe, foreign_key: true  #追記
      t.timestamps
    end
  end
end
```
```:ターミナル
rails db:migrate
```
これでlikesテーブルが作成されました。

####アソシエーションの設定
UserモデルとLikeモデルの関係は
**・ユーザーは複数お気に入り登録することができる**
**・とあるお気に入り（たとえばlikesID=1)にはユーザーが一人しかいない**
となるので

```ruby:app/models/user.rb
class User < ApplicationRecord
  has_many :recipes, dependent: :destroy
  has_many :likes #追記
#以下略
```

```ruby:app/models/like.rb
class Like < ApplicationRecord
  belongs_to :user #追記
end
```

RecipeモデルとLikeモデルの関係は
**・１つの投稿（レシピ）は複数お気に入りを持つことができる**
**・とあるお気に入り（たとえばlikesID=1)に紐づく投稿（レシピ）は１つしかない**
となるので

```ruby:app/models/recipe.rb
class Recipe < ApplicationRecord
  belongs_to :user
  has_many :likes, dependent: :destroy  #追記
#以下略
```

```ruby:app/models/like.rb
class Like < ApplicationRecord
  belongs_to :user
  belongs_to :recipe  #追記
end
```

####バリデーションの設定
ユーザーが１つの投稿に対してお気に入り登録できる回数を１回にします。
つまり、user_idとrecipe_idの組み合わせが重複しないように設定します。
[uniquenessヘルパー](https://railsguides.jp/active_record_validations.html#uniqueness)を使って重複していないか検証します。[Railsガイド](https://railsguides.jp/active_record_validations.html#uniqueness)

```ruby:app/models/like.rb
class Like < ApplicationRecord
  belongs_to :user
  belongs_to :recipe

  validates :user_id, uniqueness: { scope: :recipe_id }  #追記
end
```

####ルーティングの追加
今回、追加するルーティングはお気に入り情報を保存(create)するルーティングと削除(destroy)するルーティングになります。
また、postsに対してlikesは子の関係になるので、ネストさせどの投稿に紐づくかわかるようにします。

```ruby:config/routes.rb
Rails.application.routes.draw do
#中略
#ここから編集
  resources :recipes do 
    resources :likes, only: [:create, :destroy]
  end
#ここまで編集
end
```

####コントローラーの作成、編集
お気に入り機能に使うコントローラーを作成します。
先ほど、ルーティングでlikesと指定したのでコントローラー名をlikesとします。

```:ターミナル
rails g controller likes
```

次にアクションを追加していきます。
親モデルに属する子モデルのインスタンスを新たに生成するので[build](https://railsdoc.com/page/model_build)を使います。

```ruby:app/controllers/likes_controller.rb
class LikesController < ApplicationController
  before_action :authenticate_user!
  
  def create
    @like = current_user.likes.build(like_params)
    @recipe = @like.recipe
    if @like.valid?
      @like.save
      redirect_to recipe_path(@recipe)
    end
  end

  def destroy
    @like = Like.find(params[:id])
    @recipe = @like.recipe
    if @like.destroy
      redirect_to recipe_path(@recipe)
    end
  end

  private
  def like_params
    params.permit(:recipe_id)
  end
end
```
####インスタンスメソッドの作成
ビューファイルを編集する前に現在サインインしているユーザーがお気に入り登録しているかどうか判断するためのメソッドを作成します。

find_byでuser_idとrecipe_idが一致するlikeを探し、なければnilを返します。

```ruby:app/models/recipe.rb
class Recipe < ApplicationRecord
  belongs_to :user
  has_many :likes, dependent: :destroy
#中略
#ここから追加
  def liked_by(user)
    Like.find_by(user_id: user.id, recipe_id: id)
  end
#ここまで追加
end
```

####ビューファイルの編集
current_user != @recipe.userで自分の投稿には表示しないようにし、条件分岐に先ほど作成したliked_byメソッドを使い表示させるボタンを変化させます。

また、link_toメソッドはデフォルトではgetなのでmethod:を指定します。

```erb:app/views/recipes/show.html.erb
#中略
<div class="recipe-name">
      <%= @recipe.title %>
    </div>
    <div>
      投稿者: <%= @recipe.user.name %>さん
    </div>
    <div class="recipe-content">
      カテゴリー: <span class="recipe-category"><%= @recipe.category.name %></span>
      所要時間: <span class="recipe-time"><%= @recipe.time_required.name %></span>
    </div>

#ここから追加
    <% if current_user != @recipe.user %>
      <div class="recipe-like">
      <% if @recipe.liked_by(current_user).blank? %>
        <%= link_to "お気に入りに保存", recipe_likes_path(@recipe), method: :post, class: "btn btn-success btn-sm" %>
      <% else %>
        <%= link_to "保存済み", recipe_like_path(@recipe,@recipe.liked_by(current_user)), method: :delete, class: "btn btn-outline-success btn-sm" %>
      <% end %>
      </div>
    <% end %>
#ここまで追加
#以下略
```
以上でお気に入り登録機能が実装できました。
挙動を確認してみましょう。
![a2b9460dbe7757921f96c6dd75b8b6cb.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1140333/68e14d58-e942-550e-d8c9-8851a272703e.gif)

##お気に入りに登録したレシピを表示させよう！
ユーザーマイページにお気に入りに登録したレシピを表示させます。
####ルーティングの設定
まずは、お気に入り一覧ページのルーティングの設定をしてます。

```ruby:config/routes.rb
Rails.application.routes.draw do
#中略
#ここから編集
  resources :users do
    member do
      get :like
    end
  end
#ここまで編集
#以下略
```
####コントローラーの編集
[where](https://railsdoc.com/page/model_where)メソッドでlikesテーブルから自分のidが登録されているレコードを取得し、[pluck](https://railsdoc.com/page/model_pluck)メソッドで取得したレコードからrecipe_idを配列の形で取得します。

```ruby:app/controllers/users_controller.rb
class UsersController < ApplicationController
#中略
#ここから追加
  def like
    @user = User.find(params[:id])
    likes = Like.where(user_id: current_user.id).pluck(:recipe_id)
    @likes = Recipe.find(likes)
  end
#ここまで追加
end
```

####ビューファイルの作成、編集
まずはビューファイルを作成します。

```:ターミナル
touch app/views/users/like.html.erb
```

ビューファイルを以下のように編集します。

```erb:app/views/users/like.html.erb
<div class="like-index text-center">
  <div class="user-name">
    <%= @user.name %>さんのお気に入り
  </div>
  <% if @likes.length != 0 %>
    <% @likes.each do |recipe| %>
      <div class="recipe-contents d-flex">
        <% if recipe.images.present? %>
          <%= image_tag recipe.images[0], class: "index-img" %>
        <% else %>  
          <%= image_tag "no_image.png", class: "index-img" %>
        <% end %>
        <div class="recipe">
          <div class="recipe-title">
            <%= link_to recipe.title, recipe_path(recipe) %>
          </div>
          <div class="recipe-content">
            カテゴリー: <span class="recipe-category"><%= recipe.category.name %></span>
            所要時間: <span class="recipe-time"><%= recipe.time_required.name %></span>
          </div>
        </div>
      </div>
    <% end %>
  <% else %>
    お気に入りはまだ登録されていません
  <% end %>
</div>
```

if文でお気に入り登録を１つもしていない場合は「お気に入りはまだ登録されていません」と表示させるようにしています。
それでは実際に表示してみましょう。

#####お気に入り登録していない場合
![2f7a05068e22dd26473b723a27082aef.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1140333/3c159dc7-4991-e869-40fb-4c7eae4c6d05.png)

#####お気に入り登録している場合
![a555f7fed86f55f5613c9bab77ec753e.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1140333/f5d921ec-8e67-788e-7952-22f7d98379f6.png)

以上で完成です。