Rails has_many :through の使い方|多対多関連付け・中間テーブル
- 作成日 2026.06.27
- Ruby
Rails で「多対多の関係」を扱う時の主役が has_many :through:
class User < ApplicationRecord
has_many :memberships
has_many :groups, through: :memberships
end
class Group < ApplicationRecord
has_many :memberships
has_many :users, through: :memberships
end
class Membership < ApplicationRecord
belongs_to :user
belongs_to :group
end
ユーザーが複数のグループに所属し、グループにも複数ユーザーがいる、という典型的な多対多関係。
しかし、いざ使おうとすると:
has_and_belongs_to_many(HABTM)との違いは?- 中間テーブル(join table)はどう設計する?
sourceオプションは何を指定する?- 追加・削除のメソッドは?
includesでどう N+1 を防ぐ?- ネストした関連(
has_many :throughの連鎖)は可能? - ポリモーフィックと組み合わせられる?
など、覚えるべき作法と落とし穴が多数あります。
本記事では、has_many :through の使い方を、リファレンスとして実用的に整理します。基礎概念、マイグレーション、source/class_name/dependent オプション、追加・削除メソッド、Eager Loading、ネスト関連、ポリモーフィック、ありがちな間違い、FAQまで完全網羅。この1本でRails の多対多関連付けをマスターできます。
- 1. 結論:今すぐ使える3ステップ
- 2. まず押さえる:多対多関係の基礎
- 3. has_many :through vs has_and_belongs_to_many
- 4. マイグレーションの書き方
- 5. モデル定義の詳細
- 6. 基本的な使い方
- 7. source オプション
- 8. class_name オプション
- 9. scope と has_many :through
- 10. includes でN+1問題を解決
- 11. ネストした has_many :through
- 12. ポリモーフィック has_many :through
- 13. accepts_nested_attributes_for との組み合わせ
- 14. 中間テーブルの属性を活用
- 15. クエリのベストプラクティス
- 16. ありがちな間違い
- 17. よくある質問(FAQ)
- 17.1. Q1. has_many :through と HABTM どちらを使うべき?
- 17.2. Q2. 中間モデルの命名規則
- 17.3. Q3. through の関連にスコープを当てたい
- 17.4. Q4. 多階層の has_many :through
- 17.5. Q5. 中間テーブルの id を取得
- 17.6. Q6. ポリモーフィックと組み合わせ
- 17.7. Q7. counter_cache の使い方
- 17.8. Q8. has_one :through も存在する?
- 17.9. Q9. through で重複データを許可する
- 17.10. Q10. through 経由での << の挙動
- 17.11. Q11. dependent オプションの選択
- 17.12. Q12. テスト
- 18. 参考リンク・関連資料
- 19. まとめ
結論:今すぐ使える3ステップ
時間がない方向けに、最短のセットアップ手順を示します。
ステップ1:3つのモデルを生成
bin/rails g model User name:string
bin/rails g model Group name:string
bin/rails g model Membership user:references group:references role:string
bin/rails db:migrate
ステップ2:モデル定義
# app/models/user.rb
class User < ApplicationRecord
has_many :memberships, dependent: :destroy
has_many :groups, through: :memberships
end
# app/models/group.rb
class Group < ApplicationRecord
has_many :memberships, dependent: :destroy
has_many :users, through: :memberships
end
# app/models/membership.rb
class Membership < ApplicationRecord
belongs_to :user
belongs_to :group
end
ステップ3:使う
# グループに参加
user.groups << group
# グループの全ユーザー
group.users
# ユーザーが所属する全グループ
user.groups
# 関連の削除
user.groups.delete(group)
詳細は以下で解説します。
まず押さえる:多対多関係の基礎
1対多(has_many)
class User < ApplicationRecord
has_many :posts
end
class Post < ApplicationRecord
belongs_to :user
end
1人のユーザーが複数の投稿を持つ。投稿は1人のユーザーに属する。
多対多が必要なケース
- User と Group: ユーザーは複数グループに所属、グループも複数ユーザー
- Article と Tag: 記事は複数タグ付与、タグも複数記事に紐づく
- Movie と Actor: 映画は複数俳優、俳優は複数映画に出演
- User と Course: ユーザーは複数コース受講、コースは複数ユーザー履修
多対多の表現方法
| 方法 | 説明 | 推奨度 |
|---|---|---|
has_many :through | 中間モデルを明示 | 推奨 |
has_and_belongs_to_many(HABTM) | 中間テーブルのみ(モデルなし) | 限定的 |
公式ドキュメントもRails コミュニティも has_many :through 推奨。
has_many :through vs has_and_belongs_to_many
has_and_belongs_to_many(HABTM)
class User < ApplicationRecord
has_and_belongs_to_many :groups
end
class Group < ApplicationRecord
has_and_belongs_to_many :users
end
中間テーブル groups_users(アルファベット順)が必要:
class CreateGroupsUsersJoinTable < ActiveRecord::Migration[8.0]
def change
create_join_table :groups, :users do |t|
t.index [:user_id, :group_id]
end
end
end
has_many :through
class User < ApplicationRecord
has_many :memberships
has_many :groups, through: :memberships
end
class Group < ApplicationRecord
has_many :memberships
has_many :users, through: :memberships
end
class Membership < ApplicationRecord
belongs_to :user
belongs_to :group
end
中間モデル Membership が存在。
比較表
| 観点 | HABTM | has_many :through |
|---|---|---|
| 中間モデル | なし | あり |
| 中間テーブル | あり(アルファベット順命名) | あり(任意命名) |
| 中間テーブルへの属性追加 | △(非推奨) | ✅ 可能 |
| 中間モデルのコールバック | なし | ✅ 可能 |
| 中間モデルのバリデーション | なし | ✅ 可能 |
| 柔軟性 | 低 | 高 |
| 複雑なクエリ | 困難 | 容易 |
使い分け
- シンプルな多対多 → HABTM でも可
- 中間テーブルに属性追加・バリデーション・コールバック → has_many :through
- 将来の拡張可能性 → has_many :through(通常はこれ推奨)
公式の推奨も has_many :through。本記事ではこちらを中心に解説。
マイグレーションの書き方
基本構成
bin/rails g model User name:string
bin/rails g model Group name:string description:text
bin/rails g model Membership user:references group:references
マイグレーションの中身
class CreateMemberships < ActiveRecord::Migration[8.0]
def change
create_table :memberships do |t|
t.references :user, null: false, foreign_key: true
t.references :group, null: false, foreign_key: true
t.timestamps
end
# 同じユーザーが同じグループに二重登録を防ぐ
add_index :memberships, [:user_id, :group_id], unique: true
end
end
ポイント:
null: false: 関連がない状態を許さないforeign_key: true: 外部キー制約(参照整合性)- unique index: 重複登録防止
属性を追加する場合
class CreateMemberships < ActiveRecord::Migration[8.0]
def change
create_table :memberships do |t|
t.references :user, null: false, foreign_key: true
t.references :group, null: false, foreign_key: true
t.string :role, default: "member" # 役割
t.datetime :joined_at # 参加日時
t.boolean :active, default: true # アクティブか
t.timestamps
end
add_index :memberships, [:user_id, :group_id], unique: true
end
end
中間テーブルにビジネスロジックの属性を追加できるのが has_many :through の最大の利点。
モデル定義の詳細
基本パターン
# app/models/user.rb
class User < ApplicationRecord
has_many :memberships, dependent: :destroy
has_many :groups, through: :memberships
end
# app/models/group.rb
class Group < ApplicationRecord
has_many :memberships, dependent: :destroy
has_many :users, through: :memberships
end
# app/models/membership.rb
class Membership < ApplicationRecord
belongs_to :user
belongs_to :group
validates :user_id, uniqueness: { scope: :group_id }
end
重要な順序
class User < ApplicationRecord
# ❌ 順序が悪い(through先が未定義)
has_many :groups, through: :memberships
has_many :memberships
end
# ✅ 正しい順序
class User < ApplicationRecord
has_many :memberships # 先に中間関連
has_many :groups, through: :memberships # 後から through
end
通常は順序を気にしなくても動きますが、慣習として直接関連を先に書く。
dependent オプション
has_many :memberships, dependent: :destroy # User削除時にMembershipも削除
has_many :memberships, dependent: :delete_all # 同上(コールバックなし、高速)
has_many :memberships, dependent: :nullify # user_id を null に
has_many :memberships, dependent: :restrict_with_exception # 関連があれば削除不可
destroy_all / delete_all の違いはsave vs save! の記事参照。
バリデーション
class Membership < ApplicationRecord
belongs_to :user
belongs_to :group
# 重複防止
validates :user_id, uniqueness: { scope: :group_id, message: "は既にグループに所属しています" }
# 上限チェック
validate :max_groups_per_user
private
def max_groups_per_user
if user && user.groups.count >= 10
errors.add(:base, "最大10グループまでです")
end
end
end
基本的な使い方
関連レコードの追加
user = User.find(1)
group = Group.find(1)
# 方法1: << で追加(Membership 自動生成)
user.groups << group
# 方法2: create でMembership 経由
user.memberships.create(group: group)
# 方法3: 中間モデル経由
Membership.create(user: user, group: group)
# 中間テーブルに属性を設定する場合
user.memberships.create(group: group, role: "admin")
# 一括追加
user.groups << [group1, group2, group3]
関連レコードの取得
# user の所属グループ一覧
user.groups
# SELECT "groups".* FROM "groups"
# INNER JOIN "memberships" ON "groups"."id" = "memberships"."group_id"
# WHERE "memberships"."user_id" = 1
# group の所属ユーザー一覧
group.users
# 件数
user.groups.count
# 存在チェック
user.groups.include?(group)
user.groups.exists?(group.id)
関連レコードの削除
# 関連だけ削除(Membership だけ消える、Group は残る)
user.groups.delete(group)
# Membership と Group の関連を解除
user.memberships.find_by(group: group).destroy
# user削除(dependent: :destroy なら関連も自動削除)
user.destroy
中間レコードの直接操作
# Membership 経由でアクセス
membership = user.memberships.find_by(group: group)
membership.role = "admin"
membership.save
# 中間テーブルの全件取得
user.memberships
source オプション
source は 中間モデル内の関連名と最終モデル名が一致しない時に必要。
名前が一致するケース(source 不要)
class User < ApplicationRecord
has_many :memberships
has_many :groups, through: :memberships
# Membership に belongs_to :group があれば自動推論
end
class Membership < ApplicationRecord
belongs_to :user
belongs_to :group # ← 関連名が groups の単数形と一致
end
名前が違うケース(source 必要)
class User < ApplicationRecord
has_many :memberships
has_many :organizations, through: :memberships, source: :group
# ↑ User.organizations は Membership.group を経由する
end
class Membership < ApplicationRecord
belongs_to :user
belongs_to :group
end
User の organizations メソッドが、Membership の group 関連を経由して Group レコードを取得。
別の例:友達関係
class User < ApplicationRecord
has_many :friendships
has_many :friends, through: :friendships, source: :friend_user
end
class Friendship < ApplicationRecord
belongs_to :user
belongs_to :friend_user, class_name: "User"
end
ユーザー同士の関連で、Friendship のカラム名(friend_user_id)を反映。
source_type と組み合わせ(ポリモーフィック)
後述のポリモーフィック has_many :through で使用。
class_name オプション
関連先モデル名がメソッド名から推論できない時:
class User < ApplicationRecord
has_many :memberships
has_many :administered_groups,
through: :memberships,
source: :group,
class_name: "Group" # 明示的にクラス名
end
例:自己参照の関連
class User < ApplicationRecord
has_many :followings, foreign_key: :follower_id, class_name: "Following"
has_many :followers, foreign_key: :following_id, class_name: "Following"
has_many :followed_users, through: :followings, source: :followed
has_many :follower_users, through: :followers, source: :follower
end
class Following < ApplicationRecord
belongs_to :follower, class_name: "User"
belongs_to :followed, class_name: "User"
end
Twitter 風のフォロー機能の例。
scope と has_many :through
中間関連にスコープを当てて、目的別の関連を作る:
class Movie < ApplicationRecord
has_many :movie_crews
# 役割別のCrew取得
has_many :actor_movie_crews, -> { where(role: :actor) }, class_name: "MovieCrew"
has_many :actors, through: :actor_movie_crews, source: :crew
has_many :director_movie_crews, -> { where(role: :director) }, class_name: "MovieCrew"
has_many :directors, through: :director_movie_crews, source: :crew
end
class Crew < ApplicationRecord
has_many :movie_crews
has_many :movies, through: :movie_crews
end
class MovieCrew < ApplicationRecord
belongs_to :movie
belongs_to :crew
end
matrix = Movie.find_by(title: "Matrix")
matrix.actors # 出演俳優のみ
matrix.directors # 監督のみ
実用上非常に便利なパターン。
includes でN+1問題を解決
has_many :through は3つのテーブルを跨ぐため、N+1問題が深刻になりやすい。
N+1 が発生する例
# ❌ N+1
@groups = Group.all
@groups.each do |group|
puts group.name
puts group.users.count # ← 各 group で N+1 クエリ
end
includes で解決
# ✅ Eager Loading
@groups = Group.includes(:users)
@groups.each do |group|
puts group.name
puts group.users.count # 追加クエリなし
end
実際の SQL:
-- 1. groups 取得
SELECT * FROM groups;
-- 2. memberships を一括取得
SELECT * FROM memberships WHERE group_id IN (1, 2, 3, ...);
-- 3. users を一括取得
SELECT * FROM users WHERE id IN (...);
3クエリで完結。
中間モデル経由のincludes
# users + memberships 両方を読み込み
Group.includes(:memberships, :users)
# user → groups → posts のような階層
Group.includes(users: :posts)
詳細はincludes vs preload vs eager_load 違いの記事。
references と組み合わせ
# 中間モデルの属性で絞り込み
Group.includes(:memberships, :users)
.where(memberships: { role: "admin" })
memberships テーブルでフィルタリング。
ネストした has_many :through
class User < ApplicationRecord
has_many :memberships
has_many :groups, through: :memberships
has_many :events, through: :groups # ← さらに先
end
class Group < ApplicationRecord
has_many :events
end
class Event < ApplicationRecord
belongs_to :group
end
user = User.find(1)
user.events
# SELECT "events".* FROM "events"
# INNER JOIN "groups" ON "events"."group_id" = "groups"."id"
# INNER JOIN "memberships" ON "groups"."id" = "memberships"."group_id"
# WHERE "memberships"."user_id" = 1
ユーザーが所属するグループのイベントを1つのプロパティで取得できる。
さらに深く
class User < ApplicationRecord
has_many :memberships
has_many :groups, through: :memberships
has_many :events, through: :groups
has_many :attendees, through: :events # 3階層
end
Rails は柔軟だが、深すぎる関連はパフォーマンスとデバッグの観点で要注意。
ポリモーフィック has_many :through
class Tag < ApplicationRecord
has_many :taggings
end
class Tagging < ApplicationRecord
belongs_to :tag
belongs_to :taggable, polymorphic: true
end
class Post < ApplicationRecord
has_many :taggings, as: :taggable
has_many :tags, through: :taggings
end
class Article < ApplicationRecord
has_many :taggings, as: :taggable
has_many :tags, through: :taggings
end
Post も Article も同じ Tag システムを共有できる。
逆方向は source_type 必要
class Tag < ApplicationRecord
has_many :taggings
has_many :posts, through: :taggings, source: :taggable, source_type: "Post"
has_many :articles, through: :taggings, source: :taggable, source_type: "Article"
end
source_type で「どの型のtaggableか」を指定。
tag = Tag.find(1)
tag.posts # Postだけ取得
tag.articles # Articleだけ取得
マイグレーション
class CreateTaggings < ActiveRecord::Migration[8.0]
def change
create_table :taggings do |t|
t.references :tag, null: false, foreign_key: true
t.references :taggable, polymorphic: true, null: false
t.timestamps
end
end
end
polymorphic: true で taggable_id + taggable_type カラムが作成される。
accepts_nested_attributes_for との組み合わせ
フォームから中間レコードと一緒に保存:
class User < ApplicationRecord
has_many :memberships
has_many :groups, through: :memberships
accepts_nested_attributes_for :memberships, allow_destroy: true
end
class Membership < ApplicationRecord
belongs_to :user
belongs_to :group
end
<%= form_with model: @user do |f| %>
<%= f.fields_for :memberships do |m| %>
<%= m.collection_select :group_id, Group.all, :id, :name %>
<%= m.text_field :role %>
<%= m.check_box :_destroy %> <%# 削除フラグ %>
<% end %>
<% end %>
# controller
def user_params
params.require(:user).permit(
:name,
memberships_attributes: [:id, :group_id, :role, :_destroy]
)
end
これでフォームから中間レコードを直接追加・更新・削除できる。
詳細はsave vs save! の記事も参照。
中間テーブルの属性を活用
属性で絞り込み
class User < ApplicationRecord
has_many :memberships
has_many :groups, through: :memberships
# アクティブなメンバーシップのみ
has_many :active_memberships, -> { where(active: true) }, class_name: "Membership"
has_many :active_groups, through: :active_memberships, source: :group
end
user.active_groups # active = true の memberships を経由
ロール別
class User < ApplicationRecord
has_many :memberships
has_many :admin_memberships, -> { where(role: "admin") }, class_name: "Membership"
has_many :admin_groups, through: :admin_memberships, source: :group
has_many :member_memberships, -> { where(role: "member") }, class_name: "Membership"
has_many :member_groups, through: :member_memberships, source: :group
end
user.admin_groups # 管理者として所属しているグループ
user.member_groups # 一般メンバーとして所属しているグループ
注意:joins が外れる場合
class Post < ApplicationRecord
has_many :online_comments,
-> { joins(:source).where(sources: { online: true }) },
class_name: "Comment"
has_many :online_users, through: :online_comments, source: :user
end
⚠️ Rails のバグで through で経由する関連のスコープ内 joins が無視されることがあります。複雑な場合はテストで確実な動作確認を。
クエリのベストプラクティス
1. distinct で重複を防ぐ
# 複数 Membership がある場合、Group が重複する
Group.joins(:users).where(users: { active: true })
# ✅ distinct で重複を排除
Group.joins(:users).where(users: { active: true }).distinct
2. references の活用
# Hash 形式(references 不要)
Group.includes(:users).where(users: { active: true })
# 文字列形式(references 必要)
Group.includes(:users).where("users.active = true").references(:users)
3. 中間関連にも includes
# ❌ 中間モデル経由でアクセスする時に N+1
group.users.each { |u| puts u.memberships.first.role }
# ✅ 中間モデルも eager load
Group.includes(memberships: :user).find(group_id)
4. counter_cache 活用
class Membership < ApplicationRecord
belongs_to :user
belongs_to :group, counter_cache: :memberships_count
end
group.memberships_count で件数を高速取得(COUNT クエリ不要)。
詳細はincludes vs preload vs eager_load の記事。
ありがちな間違い
1. through 先のモデル定義漏れ
class User < ApplicationRecord
has_many :memberships
has_many :groups, through: :memberships
end
class Membership < ApplicationRecord
belongs_to :user
# belongs_to :group を書き忘れ
end
→ エラー:
ActiveRecord::HasManyThroughSourceAssociationNotFoundError
Membership に belongs_to :group の追加忘れ。
2. source の指定漏れ
class User < ApplicationRecord
has_many :memberships
has_many :organizations, through: :memberships
# ↑ Membership に :organizations はない
end
# => ActiveRecord::HasManyThroughSourceAssociationNotFoundError
→ source を追加:
has_many :organizations, through: :memberships, source: :group
3. dependent: :destroy 忘れ
class User < ApplicationRecord
has_many :memberships # ← dependent なし
has_many :groups, through: :memberships
end
User.find(1).destroy
# Membership が残ったまま(孤児レコード)
→ dependent: :destroy を追加:
has_many :memberships, dependent: :destroy
4. unique index 漏れで重複登録
# 重複登録ができてしまう
user.groups << group
user.groups << group # もう1度同じグループ
→ DBレベルで防ぐ:
add_index :memberships, [:user_id, :group_id], unique: true
- モデルでも:
validates :user_id, uniqueness: { scope: :group_id }
5. N+1 問題
# ❌
Group.all.each { |g| g.users.count }
# ✅
Group.includes(:users).each { |g| g.users.count }
6. 中間テーブルの属性を取得し忘れ
user.groups.first.name # OK
user.groups.first.role # ❌ NoMethodError(Group には role がない)
# ✅ Membership 経由
user.memberships.first.role
7. has_many :through での save! 失敗
user.groups << group
# 失敗時のエラーが取りづらい
# ✅ Membership 経由で明示
membership = user.memberships.build(group: group, role: "admin")
membership.save!
# RecordInvalid 例外で詳細が分かる
8. transactional の漏れ
# ❌ トランザクション外
user.groups << group
user.update_attributes(name: "Updated")
# 片方失敗で整合性崩壊
# ✅
ActiveRecord::Base.transaction do
user.groups << group
user.update!(name: "Updated")
end
よくある質問(FAQ)
Q1. has_many :through と HABTM どちらを使うべき?
ほぼすべての場合 has_many :through 推奨。HABTM は:
- 中間テーブルに属性が一切必要ない
- 将来的にも拡張予定なし
の場合のみ検討。
Q2. 中間モデルの命名規則
| パターン | 例 |
|---|---|
| 単純結合 | Membership(User-Group)、Enrollment(User-Course) |
| 関係名 | Friendship、Following、Subscription |
| アルファベット結合 | GroupUser(推奨されない) |
意味のある名前を付ける方が読みやすいコードに。
Q3. through の関連にスコープを当てたい
class User < ApplicationRecord
has_many :active_memberships, -> { where(active: true) }, class_name: "Membership"
has_many :active_groups, through: :active_memberships, source: :group
end
Q4. 多階層の has_many :through
class User < ApplicationRecord
has_many :memberships
has_many :groups, through: :memberships
has_many :events, through: :groups
end
技術的には可能。ただしSQLが複雑になりがちなので、用途を見極めて。
Q5. 中間テーブルの id を取得
user.memberships.find_by(group: group).id
または:
Membership.find_by(user_id: user.id, group_id: group.id).id
Q6. ポリモーフィックと組み合わせ
class Tag < ApplicationRecord
has_many :taggings
has_many :posts, through: :taggings, source: :taggable, source_type: "Post"
end
source_type で型を指定。
Q7. counter_cache の使い方
class Membership < ApplicationRecord
belongs_to :group, counter_cache: true
end
groups テーブルに memberships_count カラムを追加(マイグレーション必要):
add_column :groups, :memberships_count, :integer, default: 0
これで group.memberships_count が高速取得可能。
Q8. has_one :through も存在する?
存在します:
class User < ApplicationRecord
has_one :profile
has_one :avatar, through: :profile
end
1対1の中継関連の場合に使用。
Q9. through で重複データを許可する
# unique index を外す
remove_index :memberships, [:user_id, :group_id]
# モデルバリデーションも外す
class Membership < ApplicationRecord
belongs_to :user
belongs_to :group
# validates :user_id, uniqueness: ... を外す
end
例:同じユーザーが同じ役割で2度登録されることを許容したい時。
Q10. through 経由での << の挙動
user.groups << group
# 内部的に:
# Membership.create(user: user, group: group)
Membership が自動生成される。中間テーブルに属性が必要な場合は明示的に:
user.memberships.create(group: group, role: "admin")
Q11. dependent オプションの選択
| オプション | 動作 | 用途 |
|---|---|---|
:destroy | コールバック付き削除 | 標準的 |
:delete_all | 高速削除(コールバックなし) | 大量データ |
:nullify | foreign key を null に | 関連は切るがレコード残す |
:restrict_with_exception | 関連あれば削除不可 | 安全策 |
:restrict_with_error | 同上、エラー追加 | フォーム連動 |
Q12. テスト
RSpec.describe User do
it "groups association" do
user = User.create!(name: "Alice")
group1 = Group.create!(name: "Ruby")
group2 = Group.create!(name: "Rails")
user.groups << [group1, group2]
expect(user.groups.count).to eq(2)
expect(user.memberships.count).to eq(2)
end
end
rails console 使い方の記事で手動確認も。
参考リンク・関連資料
Rails 公式
- Active Record Associations(Guide) – 関連付け全般
- Active Record Query Interface(Guide) – クエリ
- ActiveRecord::Associations(API) – APIリファレンス
関連記事(本サイト)
- find vs find_by vs where 違い – クエリ基礎
- includes vs preload vs eager_load 違い – N+1問題
- save vs save! 違い – 保存・例外処理
- rails db:migrate 使い方 – join table マイグレーション
- rails generate 使い方 – model 生成
- rails console 使い方 – 関連の動作確認
- rails routes 見方 – nested resources
- ActiveRecord::RecordNotFound – find系エラー
- undefined method nil:NilClass – 関連先 nil 問題
- Rails 8 アップグレードガイド – 全般
- rake task 作り方 – バッチ処理での関連操作
まとめ
has_many :through は Rails の多対多関連付けの主役。要点を再整理します。
- 基本構造: 3つのモデル(A、中間、B)と、A→中間、中間→Bの関連付け
- HABTMより推奨: 中間モデルに属性・バリデーション・コールバックを持てる
- source オプション: 中間モデル内の関連名と最終モデル名が違う時に必要
- class_name オプション: モデル名がメソッド名から推論できない時
- dependent: :destroy: 親削除時に中間レコードも削除
- unique index: 重複登録防止(DB + モデル両方で)
- includes でN+1解決: 必ず使う習慣を
- distinct で重複排除: joins/includes で同じレコードが複数返る場合
- ネスト関連: has_many :through を連鎖可能
- ポリモーフィック: source_type と組み合わせ
- accepts_nested_attributes_for: フォームからの中間レコード操作
使い分けの指針
| 状況 | 推奨 |
|---|---|
| 単純な多対多 | has_many :through |
| 中間テーブルに属性 | has_many :through(必須) |
| Tag のような汎用関連 | ポリモーフィック has_many :through |
| 階層的な関連 | ネストした has_many :through |
これらの知識は、Rails アプリの設計・データモデリング・パフォーマンス改善など、あらゆる場面で活用できます。本記事をブックマークしておけば、多対多関係を実装する時の指針として役立ちます。
本記事は2026年6月時点の情報をもとに、Ruby on Rails 7.x / 8.x での動作確認・公式ドキュメントに基づき作成しています。Rails のバージョンによって挙動が異なる場合があるため、最新の情報はRails公式ガイドもあわせてご確認ください。
-
前の記事
Linuxでプロセスをバックグラウンド実行する方法|nohup・&・disown完全ガイド 2026.06.27
-
次の記事
【完全比較】Rails find vs find_by vs where の違い|戻り値・SQL・パフォーマンス・使い分けを徹底解説 2026.06.28
コメントを書く