Rails Diary

プログラミングの学習記録です。

いいね機能まとめ①(モデル作成、ルーティング)

目的

  • 星ボタンを押すとある投稿をいいねする機能の作成
  • いいねした投稿を一覧で確認できる画面の作成

今回どういった流れでどういう処理が行われ、結果どうなるのかという流れが自分にとってかなり複雑で分かりづらかった。

多対多の関係

ツイッターのようなアプリにおいて、ユーザー、投稿、いいねの機能があるとする。ユーザーと投稿には一対多の関係がある。いいね機能では、ユーザーは複数の投稿をお気に入り登録することができ、投稿側も複数のユーザーからお気に入り登録されることになる。このようにユーザー側からも投稿側からも複数に伸びる関係性を多対多の関係という。

中間テーブル

このような多対多の関係は、ユーザーと投稿の二つのテーブルだけでは実現できない。

できないことはないらしいが、ユーザー側、投稿側どちらにも外部キーを設定する必要があり、かつSQLアンチパターンとして「複数の値を一つの列に格納しない」というものがあるので、ユーザーが投稿をいいねする度に投稿テーブルにユーザーの主キーを入れるuser_id1,user_id2といったカラムを作成する必要があり、明らかに面倒くさい。

このため、ユーザーと投稿の外部キーだけを保存する中間テーブルを作成する必要がある。

実装の流れ

  1. 中間テーブルfavoriteモデルの作成

  2. アソシエーションを定義

  3. 一部メソッドをモデルに記載

  4. ルーティングの設定 ←ここまで!

  5. いいねボタンビュー作成

  6. favorites_controllerの実装

  7. いいね一覧作成

  8. 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