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

- ①のユーザーに注目すると、1人のユーザーから複数の線が伸びています。
→ つまりユーザーは「たくさんのツイートをいいね」することができます。 - ②のツイートに注目すると、1つのツイートから複数の線が伸びています。
→つまりツイートは「たくさんのユーザーにいいね」されます。
このように 「ユーザーもツイートもたくさん持っている」関係を多対多(M:N)の関係 といいます。
そして、多対多(M:N)の関係には中間テーブルが必要!
先ほどの図に中間テーブルを加えてみると

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

このようになります。
- 中間テーブルはお互いのidを保存するテーブル。
- 中間テーブルに、user_idとtweet_idを保存することで、いいねするユーザーといいねされるツイートの関係を実現しています。
こうすることで、ツイートに対して、誰にいいねされたかやいいね数を判断することができます。
Likeモデルとテーブルの作成
それでは、実装していきましょう!
以下のコマンドでモデルを作成します。
$ rails g model like user_id:integer tweet_id:integer
生成されたマイグレーションファイルを以下のように編集します。
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
1人のユーザーは1回のいいねまでとしたいので、add_index
を使って一意制約を記述します。
これで、user_idとtweet_idの組み合わせをユニーク(1通り)に設定します。
以下のコマンドでマイグレーションを実行しましょう。
$ rails db:migrate
アソシエーションの設定
それぞれのファイルに以下を追加します。
こちらはDB側と同じようにアプリケーション側にも一意制約を記述しておきましょう。
belongs_to :user
belongs_to :tweet
# 1人のユーザーは1回のいいねまでとしたいので、一意制約を記述します。
# これで、user_idとtweet_idの組み合わせをユニーク(1通り)に設定します。
validates :user_id, uniqueness: { scope: :tweet_id }
has_many :likes, dependent: :destroy
has_many :likes, dependent: :destroy
dependent: :destroy
は、has_many
で使えるオプションです。
1:Nの関係において、「1」のデータが削除された場合、関連する「N」のデータも削除される設定。
ここでは、ツイートが削除された場合に関連するいいねも削除されるように設定してます。
ルーティングの設定
いいねは、ツイートに対して、いいねするのでネストしたルーティングを作成します。
resources :tweets do
resource :likes, only: [:create, :destroy]
end
また、likeの詳細ページは作らない、つまりlikeのidは要らず省略したいため、resourceと、単数形のメソッドを利用しています。
ルーティングを見てみると
こんな感じで作成されます。
また、ネストしたルーティングを作成したことでtweet_id
はparams[:tweet_id]
で取得できます。
Controllerの作成
以下のコマンドでコントローラを作成します。
$ rails g controller likes
生成されたコントローラのファイルを以下のように編集します。
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
ユーザーがツイートをいいねしたかどうかを判定するメソッド
ユーザーがツイートをいいねしてあるかどうかを判定したいので以下のように記載します。
def liked_by?(user)
likes.exists?(user_id: user.id)
end
メソッド内のコードは、tweetに関連したいいね(like)の中で、今いいねしようとしているuser_idが存在するかどうかを判定しています。
このメソッドがtrueであればいいねを外す、falseであればいいねをするという流れでveiwを作っていきます。
Viewの作成
先ほどのliked_by?
メソッドを使って作成していきます。
= @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
で囲むことでいいねボタンを実装しています。
cssと実際の動作画面はこちらになります。
以下がcssです。
bi-heart-fill,
.liked {
color: red;
}
こうすることでいいねしている場合にアイコンといいね数を赤色に表示しています。
動作画面はこのような感じになります。
おわりに
最後まで読んでいただきありがとうございました。
少しでも皆さんの参考になれば幸いです。
参考にしたサイト