Railsで多対多のお気に入り機能を作成する

もう何度も作成していますが、未だにややこしく、ググったりしているので一度まとめようと思いました。

まず、小文字大文字、複数形などが整理できていないときは確認する。

qiita.com

中間テーブルの作成

rails g model Subscribe user:references book:references

今回はbooksテーブルとそれを登録する(各自お気に入り登録でも)usersテーブル
そしてそれの中間テーブルであるsubscribesテーブルを作成する。
(ここでは既にbooks、usersテーブルは作成済みとしている)

userとuserなど同じモデル同士で中間テーブルを作成するときは、

rails g model Subscribe user:references friend:references

などとし、migrationファイルには

t.references :friend, foreign_key: { to_table: :users }

とする。

上記を実行した後、migrationファイルに下記を追加する。

t.index [:user_id, :book_id], unique: true

これはuser_idとbook_idの重複を防ぐため。

rails db:migrate

そして実行。

各modelファイルの編集

user.rbに下記を追加

has_many :subscribes
#中間テーブルsubscribesに属するという意味
has_many :books, through: :subscribes
#中間テーブルsubscribesを通してbooksに繋がっているという意味

ここで注意しなければいけないことがあって、既に別のhas_many: :booksがある場合は別の名前にしてsourceで指定する必要がある。 例

has_many :subbooks, through :subscribes, source: :book
#source: :bookのbookはモデルを参照するので小文字単数形にする

book.rbに下記を追加

has_many :subscribes, foreign_key: 'book_id', dependent: :destroy
#中間テーブルに属するという意味、foreign_keyはbooksにあるIDしか入らないという制約をかける
#dependentはuser_idが仮に削除されたらbook_idを削除しますということ(紐づくオブジェクトを削除するということ)
has_many :users, through: :subscribes
#ここでも既に同じ名前のhas_manyがあれば仮の名前をつけてsourceで指定すること

メソッドを作成する

user.rbにメソッドを記述していく。

#登録メソッド 
def addsub(book)
  subscribes.find_or_create_by(book_id: book.id)
end

#登録解除メソッド
def removesub(book)
  subscribe = subscribes.find_by(book_id: book.id)
  subscribe.destroy if subscribe
end

#確認メソッド
def checksub?(book)
  self.books.include?(book)
end
#self.booksのbooksはhas_manyで定義したものなので、subbooksとかになってたらそうする。

この段階でrails cで動作確認をすると良い。

コントローラーの作成

rails g controller subscribes create destroy

登録機能と登録解除機能を付ける。

def create
  book = Book.find(params[:book_id])
  current_user.addsub(book)
  flash[:success] = "登録完了しました!"
  redirect_back(fallback_location: root_path)
end

def destroy
  book = Book.find(params[:book_id])
  current_user.removesub(book)
  flash[:success] = "登録解除しました!"
  redirect_back(fallback_location: root_path)
end

ルーティング

routes.rbに記述

resources :subscribes, only: [:create, :destroy]

ビュー

ここは各自用途が変わると思うので、最低限のボタンを作成してみる。

<% if current_user.checksub?(book) %>
  <%= form_with(model: current_user.subscribes.find_by(book_id: book.id), local: true, method: :delete) do |f| %>
    <%= hidden_field_tag :book_id, book.id %>
    <%= f.submit '登録解除', class: 'btn btn-danger' %>
  <% end %>
<% else %>
  <%= form_with(model: current_user.subscribes.build, local: true) do |f| %>
    <%= hidden_field_tag :book_id, book.id %>
    <%= f.submit 'お気に入り登録', class: 'btn btn-primary' %>
  <% end %>
<% end %>

最低限の機能ですが、以上です。