はじめに
初学者向けのRails講座には、大抵の場合いいね機能の実装が含まれていると思います。
ただ、いいね機能を実装するための”中間テーブル”は、概念的で理解しにくいところがあるため、自分の整理のためにもアウトプットします。
前提
- インスタ風のアプリを作成
- userがpostを投稿する感じ
- 他のuserのpostにいいねできる機能を作りたい
何がしたいのか
- いいね機能を実装したい
- postの一覧画面(post/index.html)からいいねできるようにしたい
どうする必要があるのか
- 中間テーブル(likesテーブル)を作成する
- 一意性制約をつける
- likesテーブルを通していいねしたPostを取得できるようにする
- routesの工夫
実装方法
中間テーブルを作る
- 普段通り
rails g model Like
でモデルを作る - マイグレーションファイルを編集
- reference でuserとpostを追加
- 複合インデックスを追加し、一意性制約をつける
-
rails db:migrate
の実行
マイグレーションファイルの内容
class CreateLikes < ActiveRecord::Migration[6.0]
def change
create_table :likes do |t|
t.references :user, null: false, foreign_key: true
t.references :post, null: false, foreign_key: true
t.timestamps
end
add_index :likes, [:user_id, :post_id], unique: true
end
end
中間テーブルとは?
以下のようなものだと理解しています。
- 2つのテーブルの間ある
- 2つのテーブルの関係性を記述する
- 具体的な値を持たない
今回のケースで言うと以下のようになります
- usersテーブルとpostsテーブルの間にlikesテーブルを作る
- likesテーブルのカラムは、idとuser_id(外部キー)とpost_id(外部キー)のみ
- likesテーブルはuser_id=1のユーザーが、post_id=1のポストをいいねしているという関係性のみを表現している
中間テーブルがあることで、以下のことができるようになります
- user側から:自分がいいねしたpostの一覧を取得できる
- post側から:特定のpostをいいねしているuserの一覧を取得できる
※実際のuse case的にはuser側がメインになるかなと思います
複合インデックスとは
- インデックスとは、検索速度を早めるためにデータベースに索引を作成すること
- referenceを使うと、自動的単一のインデックスが作成させる
- 今回の場合は、user_idとpost_idはほぼ必ずセットで使うので、user_idとpost_idをセットでインデックス化すると便利
-
add_index :likes, [:user_id, :post_id]
の箇所で実現している
一意性制約とは
- 特定のカラムの内容をUniqueにする制約
- よくある例でいうと同じユーザー名は使えないとか、同じメールアドレスだと"すでに登録されています"というアラートが出るとか
- 今回の場合は、特定のユーザーが特定のポストにいいねできる回数を1回にしたいので一意性制約をつける
-
unique: true
で実現 - より具体的に言うと、user_id=1 ー post_id=1 というセットがデータベースに存在すると、いいねしようとしてもできなくなる ということ
modelファイルに関係性を記述する
likeモデルの記述
class Like < ApplicationRecord
belongs_to :user
belongs_to :post
end
- likeモデル側から見ると、userにもpostにも紐づいているので上記のような記述になる
userモデルの記述
has_many :likes, dependent: :destroy
has_many :liked_posts, through: :likes, source: :post
-
has_many :likes, dependent: :destroy
これはいつもどおりの記述 -
has_many :liked_posts, through: :likes, source: :post
こちらがポイント -
User.first.liked_posts
でUser.firstがいいねしたポストを取得できるようになる -
through: :likes
はそのままlikesテーブルを経由して という記述 -
source: :post
で何を取得したいのかを記述 - おそらくlikesテーブルに外部キーが2つ設定されているため、user側からのリクエストに対してuser_idカラムを検索し、同じ行にあるpost_idを返して、postsテーブルから該当のポストを引き出すという動作を行っていそう
ほぼ重複になるのであえて書きませんが、postモデル側でも似たような記述をすることで、post側でもも特定のポストをいいねしたuserの一覧を取得できるようになる
routesの記述
resources :posts, only: [:new, :create, :index] do
resource :like, only: [:create, :destroy, :show]
end
なぜネストするのか?
- リンクとして、
post/:post_id/like
の形だと扱いやすいため - 流れとしてはuserがpostにlikeする
- あるポストに対して、いいねに対するリクエストを送る、というのがわかりやすいというか自然
なぜresources :likes
ではなく、resource :like
なのか
- あるユーザーにとって、特定のポストに対するいいねは1つだけだから
- つまり
post/:post_id/like/id
のidは不要 -
post/:post_id/like
の形にすることで、deleteする際にidを指定しなくて良いので、処理の記述も短く済む