2
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?

【Ruby on Rails】中間テーブルの作成方法、アソシエーションについて

Last updated at Posted at 2025-12-07

はじめに

こんにちは。
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. モデルファイルを編集し、アソシエーションを追加する

app/models/user_book.rb
class UserBook < ApplicationRecord
  belongs_to :user
  belongs_to :book
  
  # アプリケーションレベルの一意性バリデーション
  validates :user_id, uniqueness: { scope: :book_id }
end
app/models/user.rb
class User < ApplicationRecord
  has_many :user_books, dependent: :destroy
  has_many :books, through: :user_books
end
app/models/book.rb
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. モデルファイルを編集し、アソシエーションを追加する

app/models/bookmark.rb
class Bookmark < ApplicationRecord
  belongs_to :user
  belongs_to :board

  # アプリケーションレベルの一意性バリデーション
  validates :user_id, uniqueness: { scope: :board_id }
end
app/models/user.rb
class User < ApplicationRecord
  has_many :boards, dependent: :destroy
  has_many :bookmarks, dependent: :destroy
  has_many :bookmark_boards, through: :bookmarks, source: :board
end
app/models/board.rb
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モデルファイルを以下のように編集する。

app/models/user.rb
# 編集前(再掲)
class User < ApplicationRecord
  has_many :user_books, dependent: :destroy
  has_many :books, through: :user_books
end
app/models/user.rb
# 編集後(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は 「特定のユーザーが作成した掲示板一覧」と競合してしまう。

app/models/user.rb
# エラーとなるコード例
class User < ApplicationRecord
  has_many :boards, dependent: :destroy  # 作成した掲示板
  has_many :bookmarks, dependent: :destroy
  has_many :boards, through: :bookmarks  # ??? 同じ名前で2つ定義できない
end

そのため、2つの異なる関連を区別するために、どちらか一方の関連名を変える必要がある。

app/models/user.rb
# 選択肢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
app/models/user.rb
# 選択肢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?」「どういうこと?」となってましたが、一度まとめてみるとスッキリしますね。この記事が同様に悩んでいる方の一助となればいいなと思います。

2
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
2
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?