【Rails】いいね機能を非同期実装

# 1. はじめに
以下のデモ動画の様に、ユーザーが投稿した内容に対して"いいね"が出来る機能を実装していきます。
[![Image from Gyazo](https://i.gyazo.com/9eeae3a3b252bda35ee4b5b4639e7bdb.gif)](https://gyazo.com/9eeae3a3b252bda35ee4b5b4639e7bdb)

# 2. 前提条件
既にユーザー登録機能と投稿機能は実装されている前提で、そこに"いいね機能"を追加実装する。という流れで進めていきます。

下記の様なデータベース構造をイメージしてもらえたら分かりやすいと思います。
![ER 図(Qiita).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/645591/7a8e68e4-4e76-34fc-5712-90773d273815.png)

# 3. いいね機能の実装

**■実装するまでの流れ**

ざっくり説明すると、以下の流れで実装していきます。
・モデルの作成
　　　↓
・ルーティングの追加
　　　↓
・コントローラーの作成
　　　↓
・ビューの作成

それでは、早速いってみましょー。

## 3-1. Likeモデルの作成

まずはLikeモデルを作成します。
ターミナルで以下のコマンドを実行してください。

```:ターミナル
$ rails g model Like
```

新しくマイグレーションファイルが作成されるので、以下の通りに編集してください。

```ruby:db>migrate>xxxxxx_create_likes.rb
class CreateLikes < ActiveRecord::Migration[6.0]
  def change
    create_table :likes do |t|
      # ===追記部分===
      t.references :tweet, foreign_key: true, null: false
      t.references :user, foreign_key: true, null: false
      # ===追記部分===
      t.timestamps
    end
  end
end
```

上記の様にreferences型で保存すると、```tweet_id```と```user_id```を外部キーとして指定することが出来ます。
それではマイグレーションファイルを実行して、likesテーブルを作成しましょう。

```:ターミナル
$ rails db:migrate
```
上記のコマンドを実行した後、likesテーブルが作成されたかどうか確認して下さい。
無事作成されている事が確認出来たら、次はアソシエーションの設定です。

## 3-2. アソシエーションの設定
アソシエーションとは、2つのモデル同士の関連付けのことを指します。
UserモデルとLikeモデル、TweetモデルとLikeモデル、それぞれのアソシエーションを設定していきます。

### UserモデルとLikeモデルのアソシエーションの設定

まずはUserモデルとLikeモデルのアソシエーションを設定していきます。

２つのモデルの関係性は以下の通りです。
・ ユーザーは複数のいいねが可能
・ いいねAをしたユーザーは１人しかいない

つまり、UserモデルとLikeモデルは「１対多」の関係になります。
それでは、実際にコードを書いていきましょう。

Userモデルに以下の通りコードを追記して下さい。

```ruby:app>models>user.rb
class User < ApplicationRecord

  has_many :tweets, dependent: :destroy

  # この行を追加
  has_many :likes

end
```

```has_many``` は、他のモデルとの間に「１対多」の関係があることを示します。

次はLikeモデルに以下の通りコードを追記して下さい。

```ruby:app>models>like.rb
class Like < ApplicationRecord

  # この行を追加
  belongs_to :user

end
```

```belongs_to``` は```has_many``` の逆で、他のモデルとの間に「多対１」の関係があることを示しています。

これで、UserモデルとLikeモデルのアソシエーション設定が出来ました。

### TweetモデルとLikeモデルのアソシエーションの設定

同じ要領でTweetモデルとLikeモデルのアソシエーションも設定していきます。

２つのモデルの関係性は以下の通りです。
・ １つの投稿に対して、複数のいいねがつく
・ いいねAに紐づく投稿は１つしかない

つまり、TweetモデルとLikeモデルも「１対多」の関係になります。
それでは、実際にコードを書いていきましょう。

Tweetモデルに以下の通りコードを追記して下さい。

```ruby:app>models>tweet.rb
class Tweet < ApplicationRecord
  belongs_to :user

  # この行を追加
  has_many :likes, dependent: :destroy

end
```

```dependent: :destroy``` をつける事で、投稿が削除された時に、その投稿に紐づくいいねも削除されます。

次はLikeモデルです。

```ruby:app>models>like.rb
class Like < ApplicationRecord

  belongs_to :user

  # この行を追加
  belongs_to :tweet

end
```

以上で、全てのモデルのアソシエーションの設定が完了しました。

## 3-3. バリデーションの設定

投稿Aに対して１人のユーザーがいいねを押せる回数は1回にしたいので、1回以上は押せない様にバリデーションを設定します。

Likeモデルに以下の通り追記して下さい。

```ruby:app>models>like.rb
class Like < ApplicationRecord

  belongs_to :user
  belongs_to :tweet

  # この行を追加
  validates :user_id, uniqueness: { scope: :tweet_id }

end
```

上記の様に書く事で、```user_id``` と```tweet_id``` が重複しない様にする事が出来ます。
以上で、バリデーションの設定は完了です。


## 3-4. ルーティングの追加
いよいよ本格的にいいね機能を実装していきます。

まずは、いいね機能で使うルーティングを追加しましょう。
以下の通りコードを追記して下さい。

```ruby:config>routes.rb
Rails.application.routes.draw do
  devise_for :users,
    controllers: { registrations: 'registrations' }

  resources :tweets, only: [:index, :new, :create, :show, :destroy] do

    # この行を追加
    resources :likes, only: [:create, :destroy]

  end

end
```

いいね情報の保存と削除のルーティングを追加する必要があるので、likesコントローラーの```create``` アクションと```destroy``` アクションを定義しています。

ルーティングをネストにする事で、いいねがどの投稿に紐づくかを明示できます。

コードを追加したら```rails routes``` コマンドで、ルーティングの設定が問題ないか忘れずに確認しておきましょう。

## 3-5. likesコントローラーの作成

次にlikesコントローラーを作成していきます。
ターミナルで以下のコマンドを実行してください。

```:ターミナル
$ rails g controller likes
```
上記のコマンドを実行すると、likesコントローラーが作成できます。

それでは作成したlikesコントローラーに```create``` アクションと```destroy``` アクションを作成していきます。
以下の通りコードを追記して下さい。

```ruby:app>controllers>likes_controller.rb
class LikesController < ApplicationController

  # ===追記部分===
  def create
    @like = current_user.likes.build(like_params)
    @tweet = @like.tweet
    if @like.save
      respond_to :js
    end
  end

  def destroy
    @like = Like.find_by(id: params[:id])
    @tweet = @like.tweet
    if @like.destroy
      respond_to :js
    end
  end

  private
    def like_params
      params.permit(:tweet_id)
    end
  # ===追記部分===

end
```

privateメソッドやparamsは理解できているものとして、追加したコードについて簡単に説明していきます。

### createアクション
まず```@like``` には投稿に"いいね"をしたユーザーの```user_id``` と、"いいね"された投稿の```tweet_id``` の情報が入っています。
このコードはbuildメソッドを使って、インスタンスを作成しています。

次に```@tweet``` には```@like``` に紐づく投稿の情報、つまり"いいね"された投稿の情報が入ります。
```@tweet``` はどの投稿に"いいね"を押したのかを判断するために、ビューを作成するところで使います。

最後の```if @like.save``` の部分は、"いいね"が押された時に返すレスポンスのフォーマットを```respond_to``` メソッドで切り替えています。
"いいね"が押されたらリアルタイムでビューを反映させるために、JS形式のフォーマットでレスポンスを返すようにしています。

### destroyアクション
createアクションのところで説明した内容と重複している部分が多いので簡単に説明すると、受け取ったHTTPリクエストから```id``` を判別し、```@like``` に指定のレコードの情報を入れています。

こちらもリアルタイムでビューを反映させるために、JS形式のフォーマットでレスポンスを返すようにしています。

## 3-5. ビューの作成
いよいよビューの作成です。

まずは投稿一覧のビュー画面を編集していきましょう・・・と言いたいところですが、ビューで使うためのメソッドを先に定義しておきます。

Tweetモデルに以下の通りコードを追記して下さい。

```ruby:app>models>tweet.rb
class Tweet < ApplicationRecord
  belongs_to :user
  has_many :likes, dependent: :destroy

  # 追加部分(liked_byメソッド)
  def liked_by(user)
    Like.find_by(user_id: user.id, tweet_id: id)
  end
  # 追加部分

end
```

上記で追加した```liked_by``` メソッドは、```user_id``` と```tweet_id``` が一致するlikeを探して、無ければnillを返します。

それでは、```app/views/tweets/index.html.erb``` に以下のコードを追記して下さい。

```html:app>views>tweets>index.html.erb

<% @tweets.each do |tweet| %>

  # いいねボタンを表示したい部分に追加
  <div class="content-like">
    <ul class="content-like__icons">
      <li id="<%= tweet.id.to_s %>">
        <% if tweet.liked_by(current_user).present? %>
          <%= link_to (tweet_like_path(tweet.id, tweet.liked_by(current_user)), method: :DELETE, remote: true, class: "liked") do %>
            <i class="far fa-thumbs-up"></i>
          <% end %>
        <% else %>
          <%= link_to (tweet_likes_path(tweet), method: :POST, remote: true, class: "like") do %>
            <i class="far fa-thumbs-up"></i>
          <% end %>
        <% end %>
      </li>
    </ul>
  </div>
  # 追加部分はここまで

<% end %>

```

```liked_by``` に引数として```current_user ```を渡すことで、現在ログインしているユーザーが投稿に"いいね"をしているかどうか判断しています。

これでユーザーが"いいね"をしていない時に"いいねボタン"をクリックすると、先ほど作成した```create``` アクションを実行、ユーザーが"いいね"をしている時は```destroy``` アクションを実行と、条件分岐させる事ができました。

リンクが押された時に```.js.erb``` ファイルを呼び出す必要があるので、```link_to```に```remote: true``` オプションを追加することを忘れないでください。

なお"いいねボタン"のアイコンについては、[Font Awesome](https://fontawesome.com/) を利用しています。
導入方法については、以下のqiita記事などが参考になるかと思います。
[rails font-awesome-sass導入方法](https://qiita.com/kt215prg/items/96e582365009f42214a5)

次は、createアクションが実行された時に出力するファイルを作成します。
```app/views``` フォルダ直下に```likes``` フォルダを作成し、その中に```create.js.erb``` ファイルを作成してください。

ファイルの作成ができたら、以下の通りコードを追記してください。

```ruby:app>views>likes>create.js.erb

$('#<%= @tweet.id.to_s %>').
  html('<%= j render "tweets/liked", { tweet: @tweet } %>');
```
上記のコードで、createアクションが実行されたら```tweets``` フォルダ内の```_liked.html.erb``` ファイルを呼び出しています。

```tweets``` フォルダの中に```_liked.html.erb``` ファイルを作成し、以下のコードを追加してください。

```html:app>views>tweets>_liked.html.erb
<%= link_to (tweet_like_path(tweet.id, tweet.liked_by(current_user)), method: :DELETE, remote: true, class: "liked") do %>
  <i class="far fa-thumbs-up"></i>
<% end %>
```
上記のコードで、"いいねボタン"を押したら"いいね"を取り消すHTMLを表示するようにしています。

同じ流れで、destroyアクションが実行された時に呼び出されるファイルも作っていきましょう。
```app/views>likes``` フォルダの中に```destroy.js.erb``` ファイルを作成してください。

ファイルの作成ができたら、以下の通りコードを追記してください。

```ruby:app>views>likes>destroy.js.erb

$('#<%= @tweet.id.to_s %>').
  html('<%= j render "tweets/like", { tweet: @tweet } %>');
```

```tweets``` フォルダの中に```_like.html.erb``` ファイルを作成し、以下のコードを追加してください。

```html:app>views>tweets>_like.html.erb
<%= link_to (tweet_likes_path(tweet), method: :POST, remote: true, class: "like") do %>
  <i class="far fa-thumbs-up"></i>
<% end %>
```

以上で非同期でいいね機能を実装する事ができました。

あとは見た目ですが、クラス名を、いいねされている時は```liked``` されていない時は```like``` としていますので、CSSで自分好みにカスタマイズしてみてください。

私の場合は以下の様にして、いいねがされた時はレッド、されていない時はグレーにアイコンの色を変えています。

```css:app>assets>stylesheets>tweets>_tweet.css
.like {
  color: gray;
}

.liked {
  color: red;
}
```

# 4. さいごに

今回がはじめての投稿になりますが、記事を書くのって想像していたよりも大変ですね。
もし分かりにくい部分や、間違っている部分がある場合はご指摘いただけると嬉しいです。

最後まで読んでいただき、ありがとうございました☺️