Rails いいね機能の作り方

## はじめに
この記事はプログラミング初学者が他の記事を参考にしたり、実際に実装してみたりして、アウトプットの一環としてまとめたものです。内容に不備などあればご指摘いただますと幸いです。<br>
今回Xクローン作成中にいいね機能を実装しました。
そのいいね機能について備忘録も兼ねて記事を作成していきます。<br>
また、リツイート機能も同じ手順で実装することができます。

## 実現したいこと
投稿されたツイートに対して、いいねをつけたり、外したりできる機能を実装すること。

## いいね機能について
#### いいねする時のイメージ
<img width="500" alt="like_1.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3628757/273ec8b8-63a1-4e7e-b683-2f4fdc320ce9.png">

* ①のユーザーに注目すると、１人のユーザーから複数の線が伸びています。
→ つまりユーザーは「たくさんのツイートをいいね」することができます。
* ②のツイートに注目すると、１つのツイートから複数の線が伸びています。
→つまりツイートは「たくさんのユーザーにいいね」されます。

このように 「ユーザーもツイートもたくさん持っている」関係を多対多(M:N)の関係 といいます。
そして、多対多(M:N)の関係には中間テーブルが必要！

#### 先ほどの図に中間テーブルを加えてみると
<img width="600" alt="like_2.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3628757/21e45362-cb71-4813-916d-2fc643095d7b.png">

このようになります。
* 中間テーブルでは、いいねするユーザーといいねされるツイートの関係を保存しています。
* また、いいねするユーザーと中間テーブルが1対多。
  いいねされるツイートと中間テーブルが1対多。
  つまり、中間テーブルを介することで多対多を1対多で表現することができます。

#### 中間テーブルを用いてER図を表すと
<img width="700" alt="like_er.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3628757/9463378e-5a86-4e88-9201-2b8c0ff66ddc.png">

このようになります。
* 中間テーブルはお互いのidを保存するテーブル。
* 中間テーブルに、user_idとtweet_idを保存することで、いいねするユーザーといいねされるツイートの関係を実現しています。
  こうすることで、ツイートに対して、誰にいいねされたかやいいね数を判断することができます。


## Likeモデルとテーブルの作成
それでは、実装していきましょう！
以下のコマンドでモデルを作成します。
```
$ rails g model like user_id:integer tweet_id:integer
```
生成されたマイグレーションファイルを以下のように編集します。
```ruby:db/migrate/migrate/○○○_create_likes.rb
class CreateLikes < ActiveRecord::Migration[7.0]
  def change
    create_table :likes do |t|
      t.integer :user_id
      t.integer :tweet_id

      t.timestamps
    end

    add_index :likes, %i[user_id tweet_id], unique: true # 追加
  end
end
```
１人のユーザーは１回のいいねまでとしたいので、`add_index`を使って一意制約を記述します。
これで、user_idとtweet_idの組み合わせをユニーク(１通り)に設定します。<br>
以下のコマンドでマイグレーションを実行しましょう。
```
$ rails db:migrate
```

## アソシエーションの設定
それぞれのファイルに以下を追加します。
こちらはDB側と同じようにアプリケーション側にも一意制約を記述しておきましょう。
```ruby:app/models/like.rb
  belongs_to :user
  belongs_to :tweet
  
  # １人のユーザーは１回のいいねまでとしたいので、一意制約を記述します。
  # これで、user_idとtweet_idの組み合わせをユニーク(１通り)に設定します。
  validates :user_id, uniqueness: { scope: :tweet_id }
```
```ruby:app/models/user.rb
has_many :likes, dependent: :destroy
```
```ruby:app/models/tweet.rb
has_many :likes, dependent: :destroy
```
`dependent: :destroy`は、`has_many`で使えるオプションです。
1:Nの関係において、「1」のデータが削除された場合、関連する「N」のデータも削除される設定。
ここでは、ツイートが削除された場合に関連するいいねも削除されるように設定してます。

## ルーティングの設定
いいねは、ツイートに対して、いいねするのでネストしたルーティングを作成します。
```ruby:config/routes.rb
resources :tweets do
  resource :likes, only: [:create, :destroy]
end
```
また、likeの詳細ページは作らない、つまりlikeのidは要らず省略したいため、resourceと、単数形のメソッドを利用しています。<br>
ルーティングを見てみると
![スクリーンショット 2025-05-30 18.50.07.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3628757/a623006d-a203-4a18-b904-537b9efb327c.png)
こんな感じで作成されます。
また、ネストしたルーティングを作成したことで`tweet_id`は`params[:tweet_id]`で取得できます。

## Controllerの作成
以下のコマンドでコントローラを作成します。
```
$ rails g controller likes
```
生成されたコントローラのファイルを以下のように編集します。
```ruby:app/controllers/likes_controller.rb
class LikesController < ApplicationController
  def create
    # current_userに関連したLikeクラスの新しいインスタンスを作成。
    # つまり、like.user_id = current_user.idが済んだ状態で生成されている。
    # buildはnewと同じ意味で、アソシエーションしながらインスタンスをnewする時に形式的に使われる。
    @like = current_user.likes.build(tweet_id: params[:tweet_id])
    @like.save!
    # redirect_backを使うと、直前のページにリダイレクトをしてくれる。
    # fallback_locationには直前のページに戻れなかった際のパスを記載する。
    redirect_back(fallback_location: root_path)
  end

  def destroy
    @like = current_user.likes.find_by(tweet_id: params[:tweet_id])
    @like.destroy!
    redirect_back(fallback_location: root_path)
  end
end
```

## ユーザーがツイートをいいねしたかどうかを判定するメソッド
ユーザーがツイートをいいねしてあるかどうかを判定したいので以下のように記載します。
```ruby:app/models/tweet.rb
def liked_by?(user)
  likes.exists?(user_id: user.id)
end
```
メソッド内のコードは、tweetに関連したいいね(like)の中で、今いいねしようとしているuser_idが存在するかどうかを判定しています。
このメソッドがtrueであればいいねを外す、falseであればいいねをするという流れでveiwを作っていきます。

## Viewの作成
先ほどの`liked_by?`メソッドを使って作成していきます。
```ruby:app/views/tweets/index.html.slim
= @tweets.each do |tweet|
  div.d-flex.align-items-center
    - if tweet.liked_by?(current_user) # ユーザーがいいねしたかどうかで分岐 
      # いいねしている場合以下を表示
      = link_to tweet_likes_path(tweet.id), data: { turbo_method: :delete } do
        i.bi.bi-heart-fill
    - else
      # いいねしていない場合以下を表示
      = link_to tweet_likes_path(tweet.id), data: { turbo_method: :post } do
        i.bi.bi-heart
    span class="#{'liked' if tweet.liked_by?(current_user)}"
      = tweet.likes.count # countメソッドを使っていいねの数を表示
```
bootstrapのアイコンを`link_to`で囲むことでいいねボタンを実装しています。<br>
cssと実際の動作画面はこちらになります。
以下がcssです。
```scss:app/assets/stylesheets/application.bootstrap.scss
bi-heart-fill,
.liked {
  color: red;
}
```
こうすることでいいねしている場合にアイコンといいね数を赤色に表示しています。<br>
動作画面はこのような感じになります。
<img width="700" alt="like.gif" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3628757/c4d0392d-6a9a-4374-91e0-80652b975aae.gif">

## おわりに
最後まで読んでいただきありがとうございました。
少しでも皆さんの参考になれば幸いです。


## 参考にしたサイト

https://qiita.com/kazukimatsumoto/items/14bdff681ec5ddac26d1

https://zenn.dev/goldsaya/articles/0afed63ced4b55

https://youtu.be/7XooLWaDYrw?si=mUIpNuqs1ymnlvRT