はじめに
こんにちは。
RUNTEQにて学習中のかるめと申します。
今回の記事は、私がRailsでの中間テーブルについて学習していると頭がごちゃごちゃしてきたため、まとめてみたものとなります。
中間テーブル
ブックマーク機能を作成する場合など、多対多の関係を表現するには、中間テーブルを使う。
アソシエーション
モデルファイルのアソシエーションの書き方は以下のような場合によって異なる。
- 直接モデル間で多対多となっている場合(対称的な多対多関係)
- モデル間では1対多だが、ブックマーク機能のような追加機能を通すと多対多となる場合(非対称的な多対多関係)
パターンA: 対称的な多対多関係の場合
例:UserモデルとBookモデル
- 直接的に多対多となっている
- 1人のユーザーが複数の本を持つ
- 1つの本は複数のユーザーが持つ
作成手順
1. 中間テーブルの生成、マイグレーションファイルの編集
モデルを生成する。
rails g model UserBook
マイグレーションファイルを編集する。
class CreateUserBooks < ActiveRecord::Migration[7.0]
def change
create_table :user_books do |t|
t.references :user, foreign_key: true
t.references :book, foreign_key: true
t.timestamps
end
# 複合インデックス(データベースレベルの一意性制約付き)
add_index :user_books, [:user_id, :book_id], unique: true
end
end
2. マイグレーションの適用
rails db:migrate
3. モデルファイルを編集し、アソシエーションを追加する
class UserBook < ApplicationRecord
belongs_to :user
belongs_to :book
# アプリケーションレベルの一意性バリデーション
validates :user_id, uniqueness: { scope: :book_id }
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
Userモデルファイルのhas_many :books, through: :user_books によりuser.booksで「特定のユーザーが持っている本一覧」を取得できる。また、Bookモデルの has_many :users, through: :user_books により、book.usersで「特定の本を持っているユーザー一覧」を取得できる。
パターンB: 非対称的な多対多関係の場合
例:UserモデルとBoard(掲示板)モデル
- モデル間では1対多だが、ブックマーク機能を通すと多対多となる
- 1人のユーザーが複数の掲示板を作成できる
- 1人のユーザーが複数の掲示板をブックマークできる
- 1つの掲示板は複数のユーザーにブックマークされる
作成手順
1. 中間テーブル(Bookmarkモデル)の生成、マイグレーションファイルの編集
モデルを生成する。
rails g model Bookmark
マイグレーションファイルを編集する。
class CreateBookmarks < ActiveRecord::Migration[7.0]
def change
create_table :bookmarks do |t|
t.references :user, foreign_key: true
t.references :board, foreign_key: true
t.timestamps
end
# 複合インデックス(データベースレベルの一意性制約付き)
add_index :bookmarks, [:user_id, :board_id], unique: true
end
end
2. マイグレーションの適用
rails db:migrate
3. モデルファイルを編集し、アソシエーションを追加する
class Bookmark < ApplicationRecord
belongs_to :user
belongs_to :board
# アプリケーションレベルの一意性バリデーション
validates :user_id, uniqueness: { scope: :board_id }
end
class User < ApplicationRecord
has_many :boards, dependent: :destroy
has_many :bookmarks, dependent: :destroy
has_many :bookmark_boards, through: :bookmarks, source: :board
end
class Board < ApplicationRecord
belongs_to :user
has_many :bookmarks, dependent: :destroy
has_many :bookmark_users, through: :bookmarks, source: :user
end
Userモデルの has_many :bookmark_boards, through: :bookmarks, source: :board によりuser.bookmark_boardsで「特定のユーザーがブックマークした掲示板一覧」を取得できる。また、Boardモデルの has_many :bookmark_users, through: :bookmarks, source: :user によりboard.bookmark_usersで「特定の掲示板をブックマークしたユーザー一覧」を取得できる(sourceに関しては下記 sourceについてを参照)。
dependent: :destroyについて
dependent: :destroy は直接的な関係にのみ付ける。
throughで定義される関係は間接的なものなので、付けても効果は無い。
sourceについて
Railsではthroughによって関連付けると自動的に関連名が決まる。
関連名を自動で決まるものを使わずに自分で作成して、よりわかりやすくしたい場合にsourceを使う。
例:パターンA
Railsにより自動的にuser.booksで「特定のユーザーの本一覧」を取得できる。
これをuser.read_booksのように、「特定のユーザーが読んだ本一覧」であると、よりわかりやすくしたい場合はUserモデルファイルを以下のように編集する。
# 編集前(再掲)
class User < ApplicationRecord
has_many :user_books, dependent: :destroy
has_many :books, through: :user_books
end
# 編集後(sourceを使う場合)
class User < ApplicationRecord
has_many :user_books, dependent: :destroy
has_many :read_books, through: :user_books, source: :book
end
例:パターンB
sourceを使わない場合、Railsにより自動的に user.boardsで「特定のユーザーがブックマークした掲示板一覧」を取得できそう・・・であるが、 UserモデルとBoardモデルは既に直接の関係として存在しているため、 user.boardsは 「特定のユーザーが作成した掲示板一覧」と競合してしまう。
# エラーとなるコード例
class User < ApplicationRecord
has_many :boards, dependent: :destroy # 作成した掲示板
has_many :bookmarks, dependent: :destroy
has_many :boards, through: :bookmarks # ??? 同じ名前で2つ定義できない
end
そのため、2つの異なる関連を区別するために、どちらか一方の関連名を変える必要がある。
# 選択肢1: 直接の関連名を変える(sourceを使わない)
# user.created_boardsで「特定のユーザーが作成した掲示板」を取得
# user.boardsで「特定のユーザーがブックマークした掲示板」を取得
class User < ApplicationRecord
has_many :created_boards, class_name: 'Board', dependent: :destroy # class_nameが必要
has_many :boards, through: :bookmarks # sourceは不要
end
# 選択肢2:sourceを使った書き方(再掲)
# user.boardsで「特定のユーザーが作成した掲示板」を取得
# user.bookmark_boardsで「特定のユーザーがブックマークした掲示板」を取得
class User < ApplicationRecord
has_many :boards, dependent: :destroy
has_many :bookmarks, dependent: :destroy
has_many :bookmark_boards, through: :bookmarks, source: :board
end
おわりに
多対多の関係はすごく複雑なので、学習中「through?」「source?」「どういうこと?」となってましたが、一度まとめてみるとスッキリしますね。この記事が同様に悩んでいる方の一助となればいいなと思います。