Railsで「いいね！」機能を作る - ②「いいね！」のcreateアクション

### 背景
Twitterのようなアプリを作っていて、投稿に「いいね！」ボタンを付けようと思います。
作ろうとしているもののデータベース構造はこんな感じです。

▼ER図
[![Image from Gyazo](https://i.gyazo.com/fcef6fe2644d5ddbf2275841de1d7ca0.png)](https://gyazo.com/fcef6fe2644d5ddbf2275841de1d7ca0)

作ろうとしている画面の参考画像はこちらです
▼画像イメージ
[![Image from Gyazo](https://i.gyazo.com/3af335cfe1a45eee10df6ef54b24bc3e.png)](https://gyazo.com/3af335cfe1a45eee10df6ef54b24bc3e)

この「いいね！」ボタンを押すと、ユーザーが投稿に「いいね！」でき、もう一度ボタンを押すと「いいね！」が解除できるようにしたいです。

モデル間のアソシエーションの設定は、過去にこの記事で行ってきました。  

▼アソシエーションの設定
[Railsで「いいね！」機能を作る - ①アソシエーションに別名をつける](https://qiita.com/tanutanu/items/2a27f1e503c4cbf30928)

今回は、controllerの記述を中心に紹介し、ボタンを押すと「いいね！」ができるところまでを目標にしてみたいと思います。なお、「いいね！」の解除機能は、この次の記事で実装します。

▼この記事の続編はこちら
[Railsで「いいね！」機能を作る - ③「いいね！」を解除できるようにする](https://qiita.com/tanutanu/items/89cd52ed57b487fed84d)

### ゴール
ゴールは至ってシンプルで、`users`と`posts`の中間テーブルである`likes`に適切なデータが入れば良いだけです。

![er.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/393888/a580882f-7f3f-3a30-a49f-1080f9602f44.png)
入れるデータは**ログインしているユーザーのid**と、**「いいね！」ボタンを押した投稿のid**です。

ただし、**ユーザーがすでに「いいね！」を押した投稿に対し、重複して「いいね！」を押すことはできません。**←この条件が結構難しくてハマりどころではないのかと思います。

### とりあえず動かす① - 中間テーブルにデータを保存できるようにする
#### controller
とりあえず、中間テーブルにデータを保存するところからスタートしてみます。`likes`テーブルにシンプルに`user_id`と`post_id`を保存するようにしてみました。

```ruby:controllers/likes_controller.rb
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)`は「元のページに戻る」コードです。詳細は[こちらの記事](https://qiita.com/100010/items/3ae5dea25e85b0cbf1ee)をご覧いただくのがわかり易いと思います。

#### view
ビューは、下図のように投稿のパーシャルの中に「いいね！」ボタンをつける想定です。私は以下のように書きました。

```erb:views/posts/_post.rb
<!-- 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">]> 
```

...同じユーザーが同じ投稿に対して持っている「いいね！」が２つになっています。。。これではいけませんね。。。

### とりあえず動かす② - 同じ投稿に「いいね！」が２回できないようにする
そこで、ユーザーが「いいね！」をした投稿(`favorites`)の中に、たった今、画面上で「いいね！」をされた投稿(`clicked_post`)がないか探し、なければlikesテーブルにデータを保存するようにしたいと思います。

```Ruby:controllers/likes_controller.rb
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`の定義は、[前回のこちらの記事](https://qiita.com/tanutanu/items/2a27f1e503c4cbf30928)を見てください。

これで、**同じ投稿に２回「いいね！」できないようにする**という条件はクリアしました。

...が、コードが汚いです:sweat_smile:
コントローラーに色々なものを書きすぎています。  
コントローラーはできるだけ薄くするのがRailsによるアプリ作りの原則ですので、リファクタリング（コードを簡素化し、きれいにする）をしたいと思います。

### リファクタリング
結論から言うと、リファクタリングの結果作成したのは、以下のコードです。

```ruby:controllers/likes_controller.rb
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
```
```ruby:models/user.rb
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`の仕様は[こちらの記事](http://campusnote.hatenablog.com/entry/2017/11/19/214810)がわかりやすかったです。

このメソッドで、コントローラーにゴニョゴニョあったif文は全て消えてしまいました。わー。。。。:sweat_smile:リファクタリング、大事です。

あとは、地味ですがこちらの部分もリファクタリングしました。

```ruby
# これを
current_user.like.new(post_id: clicked_post.id)

# こうした
current_user.like.new(post: clicked_post)
```
`*_id` 同士を比べて、、、、という書き方は、基本的に全てインスタンス同士を比較する書き方に書き換えられそうです。

ここまでやったら、かなりRailsに詳しくなれそうです。。。。

次は、一度「いいね！」を押したボタンをクリックすると、「いいね！」を取りやめる、「いいね！」の削除機能について調べていきます。

▼こちらの記事です
[Railsで「いいね！」機能を作る - ③「いいね！」を解除できるようにする](https://qiita.com/tanutanu/items/89cd52ed57b487fed84d)

### 補足：マイグレーションを作るときに安全性をあげる

なお、順番が前後してしまいましたが、「同じ投稿に２回『いいね！』することを避ける」ために、DB側に以下のような設定もできます。

```ruby
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 一部のコードをリファクタリングしました。