0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Rails】中間テーブル

Posted at

はじめに

中間テーブル作成時に調べた内容をまとめます。

中間テーブルとは

多対多の関係を管理するためのテーブル

定義

  • UserモデルとBookモデルを紐づける中間モデルUserBookの作成を想定

マイグレーションファイル

class CreateUserBooks < ActiveRecord::Migration[6.1]
  def change
    create_table :user_books do |t|
      t.references :user, null: false, foreign_key: { on_delete: :cascade }
      t.references :book, null: false, foreign_key: { on_delete: :cascade }

      t.timestamps
    end

    add_index :user_books, [:user_id, :book_id], unique: true
  end
end

モデルファイル

class User < ApplicationRecord
  has_many :user_books, dependent: :destroy
  has_many :books, through: :user_books
end
class Book < ApplicationRecord
  has_many :user_books, dependent: :destroy
  has_many :users, through: :user_books
end
class UserBook < ApplicationRecord
  belongs_to :user
  belongs_to :book

  validates :user_id, uniqueness: { scope: :book_id }
end

dependent: :destroyforeign_key: {on_delete: :cascade}

  • dependent: :destroy はアプリケーションレベルで、親オブジェクト削除時に関連する子オブジェクトのdestroyメソッドを呼び、コールバックも実行する
  • foreign_key: {on_delete: :cascade}はDBレベルで、親レコードが削除されると自動的にSQLの ON DELETE CASCADE が実行され関連する子レコードを削除することで、整合性が保証される

validates :user_id, uniqueness: { scope: :book_id }add_index :user_books, [:user_id, :book_id], unique: true

  • validates :user_id, uniqueness: { scope: :book_id } はアプリケーションレベルで、ユーザが同じ本を重複して登録しようとした場合にバリデーションエラーを出す。バリデーションメッセージが表示される
  • add_index :user_books, [:user_id, :book_id], unique: true はDBレベルで、user_id と book_id の組み合わせの重複を防ぐための一意制約付きインデックスを追加する。これにより、同じユーザーが同じ本を複数回登録することをDBが拒否する

登録

UserBook.create(user: user, book: book, rating: 5)

取得

  • userが評価5を付けた本を取得する場合を想定

方法①(SQLクエリは1回:最も効率的)

user.books.merge(UserBook.where(rating: 5))
  • user.bookshas_many: books, through: :user_books のアソシエーションによりuserに紐づくbooksを取得
  • .merge(UserBook.where(rating: 5)):user_booksテーブルのraiting=5の条件がJOINに適用される
発行されるSQL
SELECT books.*
FROM books
INNER JOIN user_books ON user_books.book_id = books.id
WHERE user_books.user_id = ? AND user_books.rating = 5

方法②(SQLクエリは2回)

UserBook.includes(:book).where(user: user, rating: 5).map(&:book)
  • UserBook.includes(:book):UserBookモデルの中で定義されたbelongs_to: book を元に、関連するBookを先にまとめて取得する(イーガーローディング)
  • .where(user: user, rating: 5)
    • 該当するUserBookレコードを取得
発行されるSQL
SELECT "user_books".* FROM "user_books"
WHERE "user_books"."user_id" = ? AND "user_books"."rating" = 5
SELECT "books".* FROM "books"
WHERE "books"."id" IN (?, ?, ?, ...)
  • IN句には先ほど取得したUserBookのbook_idがすべて入る
  • map(&:book):この時点でbookはすでにメモリ上に読み込まれているので追加のSQLは発行されない

方法③(N+1問題が発生)

UserBook.where(user: user, rating: 5).map(&:book)
  • UserBook.where(user: user, rating: 5) :user_booksテーブルから該当レコードを取得(1クエリ)
  • .map(&:book):各レコードに対して.bookを呼ぶたびにbooksテーブルにアクセスする(Nクエリ)
発行されるSQL
SELECT "user_books".* 
FROM "user_books" 
WHERE "user_books"."user_id" = ? AND "user_books"."rating" = 5;
SELECT "books".* 
FROM "books" 
WHERE "books"."id" = ? 
LIMIT 1;
  • 1回目のSQLで取得した各UserBookレコードに対して.bookを呼ぶたびにクエリが発行される

削除

  • 中間レコードだけ削除する場合
user_book = UserBook.find_by(user: user, book: book)
user_book.destroy
user.books.delete(book)
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?