いいね機能まとめ①(モデル作成、ルーティング)
目的
- 星ボタンを押すとある投稿をいいねする機能の作成
- いいねした投稿を一覧で確認できる画面の作成
今回どういった流れでどういう処理が行われ、結果どうなるのかという流れが自分にとってかなり複雑で分かりづらかった。
多対多の関係
ツイッターのようなアプリにおいて、ユーザー、投稿、いいねの機能があるとする。ユーザーと投稿には一対多の関係がある。いいね機能では、ユーザーは複数の投稿をお気に入り登録することができ、投稿側も複数のユーザーからお気に入り登録されることになる。このようにユーザー側からも投稿側からも複数に伸びる関係性を多対多の関係という。
中間テーブル
このような多対多の関係は、ユーザーと投稿の二つのテーブルだけでは実現できない。
できないことはないらしいが、ユーザー側、投稿側どちらにも外部キーを設定する必要があり、かつSQLのアンチパターンとして「複数の値を一つの列に格納しない」というものがあるので、ユーザーが投稿をいいねする度に投稿テーブルにユーザーの主キーを入れるuser_id1,user_id2といったカラムを作成する必要があり、明らかに面倒くさい。
このため、ユーザーと投稿の外部キーだけを保存する中間テーブルを作成する必要がある。
実装の流れ
中間テーブルfavoriteモデルの作成
アソシエーションを定義
一部メソッドをモデルに記載
ルーティングの設定 ←ここまで!
いいねボタンビュー作成
favorites_controllerの実装
いいね一覧作成
N+1問題対策
1. 中間テーブルFavoriteモデルの作成
① 外部キー制約を付けたFavoriteモデルの作成
bundle exec rails g model Favorite user:references post:references
② マイグレーションファイルを編集する
class CreateFavorites < ActiveRecord::Migration[5.2] def change create_table :favorites 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
↑あとでモデルにも一意性確保のバリデーションを追加するが、それだけでは不十分なのでDB側にもuser_id,post_idカラムに一意インデックスを作成する。
t.index [:user_id, :post_id], unique: true
の記載で[:user_id, :post_id]
の組み合わせの一意性を確保している。
create_tableのブロックから外して、add_index :favorites, [:user_id, :post_id], unique: trueと書くことも出来るっぽい?
rails4からrails5でインデックスの貼り方が変わった? - 旅好きの気ままなお話
③ モデルにバリデーションを追加
favorite.rb
class Favorite < ApplicationRecord belongs_to :user # 自動的に記載 belongs_to :post # 自動的に記載 validates :user_id, uniqueness: { scope: :board_id } #追加 end
uniquenessヘルパーはuniqueness: true
のように記載することで、属性の値が重複していないかどうかを検証する。そしてscopeオプションを用いることで一意性チェックを範囲を限定することができる。
使用例 2.12 uniqueness | Railsガイドより
祝日の名前を一年間の内重複しないようにしているんだと思う。
class Holiday < ApplicationRecord validates :name, uniqueness: { scope: :year, message: "発生は年に1度である必要があります" } end
正直100%ピンときている訳ではないけど、使用例のイメージとしてはこんな感じ。。
2. アソシエーションの追加
favorite.rbはモデル作成時にカラムのデータ型をreferencesにしたことで、belongs_toが自動記載される
belongs_toの設定により、対象カラムに対する
presence: true
が自動付与されるため記載不要。post.rbには
has_many :favorites(複数形), dependent: :destroy
と記載。 →dependent: :destroy
の追加により、投稿を削除すればいいねも削除される。関連付けしたDBの整合性を保つ目的。
(ある値をAテーブルからは削除したのにBテーブルには残っていると不整合のため、片方のテーブルから消した場合、もう片方のテーブルからも削除されるように設定)
user.rb
has_many :posts, dependent: :destroy has_many :replies, dependent: :destroy has_many :favorites, dependent: :destroy has_many :favorite_posts, through: :favorites, source: :post
↑突然現れたfavorite_postsは何者?
最後の行has_many :favorite_posts, through: :favorites, source: :post
の記述
関連付けではhas_many :モデル名
という形でモデル名を記載するものと思っていたので、favorite_posts
というモデルを新たに作る必要があるのかと困惑した。モデル名の箇所はあくまでも関連先の名称であり、訳あってモデル名が使えない時はsourceオプションを付与することで名称変更ができる。
前提として、本来はhas_many :posts, through: :favorites
という形で、favorites(中間テーブル)を経由して、いいねしたpostsを持っている旨を記述をしたかった。しかし、postsという関連先名は1行目のuserとpostのアソシエーションで既に使っているため、重複を避ける目的で新たにfavorite_postsという名称に変えて使用している。名称を変えたので、本来の参照元であるpostをsourceオプションを用いて末尾に記載している。
アソシエーションでhas_many :favorite_posts, through: :favorites, source: post
と記載したことでいいねした投稿を簡単に取得することができる。
@user=User.find(1) @favorite_posts=@user.favorite_posts # あるユーザーのいいねした投稿を全て取得
3. user.rbに記載したギミック
一部処理をコントローラではなく、モデルに記載することでファットコントローラを防ぐことができる。モデルにメソッドを定義し、コントローラで呼び出す方法はよく使われるらしい。user.rbに記載しているのはuser.rbに記載したアソシエーションであるfavorite_postsを用いるため。
モデルに記載したアソシエーション、has_many :posts
のように複数形になっていることからも分かるが、@user.postsは複数の投稿が入った配列になっている。
favoriteメソッド
user(レシーバ).favorite_postsという配列に引数で指定したpostを追加している
def favorite(post) favorite_posts << post end
favoritesコントローラのcreateメソッドでcurrent_user.favorites.create(post_id: params[:id])
と書くよりも、user.rbのアソシエーションを利用したギミックを使った方が可読性が高い。
favorite_post << post
のように<<
演算子を使うことでfavorites.create!(post_id: post.id)
と同様の処理が行われ、CREATE文のSQLが発行される。
詳しくは4.3.1 has_manyで追加されるメソッド | Railsガイドを確認!
def create post=Post.find(params[:post_id]) current_user.favorite(post) (略) end
current_userに対し、favoriteメソッドを呼び出すことでcurrent_user.favorite_posts
という配列に引数で渡したpostを追加している形になる。
unfavoriteメソッド
user.favorite_postsという配列から指定したpostを削除している
def unfavorite(post) favorite_posts.delete(post) end
favorite?メソッド
favorite_postsという配列に指定したpostが含まれているか判定する
def favorite?(post) favorite_posts.include?(post) end
4. ルーティングの設定
resources :posts do resources :comments, only: %i[create], shallow: true get :favorites, on: :collection end resources :favorite, only: %i[create destroy]
- collectionを用いることで、index, new, show, edit, update, destroy以外のアクションへのルーティングが作成できる。今回は
posts/favorites
という直感的に分かりやすいURLを作成したいので、collectionを用いて上記のように作成する。 - resources :favoriteをpostにネストさせない理由は、リクエストにパラメータを付与するやり方を学ぶため。(詳しくは後述)
以下のネストする書き方でも可
resources :posts do resources :comments, only: %i[create], shallow: true resource :favorite, only: %i[create destroy] get :bookmarks, on: :collection end
resource :favorite
とresourceを単数にすることでfavorite自身のidを省略できる- この書き方の場合、リクエストにパラメータを付与する必要はなくなる。
参考にしたサイト
SQLアンチパターン 複数の値を一つの列に格納するな : ITフロギストン~技術系ブログみたいなもの
掲示板にお気に入り機能を実装する① - Programming Learning Diary
https://railsguides.jp/active_record_validations.html#uniqueness