Railsの関連付けメソッドについて、違いがちゃんと理解できておらず調べてることが多かったので、今回まとめ記事を作りました。
なぜ関連付けメソッドが必要なのか?
・N+1問題を解決するため
下記のコードでは、取得した本の数だけクエリの発行数が増えることになってしまいます。
そのため、後ほど解説するメソッドでレコードをキャッシュし、クエリの発行数を減らします。
# N+1問題の例
books = Book.all
books.each do |book|
puts book.author.name # 各bookに対してクエリが発行される
end
# SELECT * FROM books (1クエリ)
# SELECT * FROM authors WHERE id = 1 (+N クエリ)
# SELECT * FROM authors WHERE id = 2
# SELECT * FROM authors WHERE id = 3
# ...
・関連テーブルで条件で絞り込むため
関連テーブルのカラムを使って絞り込みを効率的に行うために使用します。
# 著者名が Alice の本に絞り込む
books = Book.joins(:author).where(authors: { name: "Alice" })
各メソッドの違い
| メソッド | 関連データ取得 (キャッシュ) |
SQL実行回数 | 主な使用場面 |
|---|---|---|---|
| joins | × | 1回 | 関連テーブルでの絞り込み |
| preload | ○ | 複数回 | 関連データを使用する場合 |
| eager_load | ○ | 1回 | 関連データ使用+絞り込みをする場合 |
| includes | ○ | 自動選択 | 条件によって挙動が自動で切り替わる |
以降、詳しく各メソッドについて見ていきます。
1. joins
Book.joins(:reviews)
# SELECT books.* FROM books INNER JOIN reviews ON reviews.book_id = books.id
INNER JOIN なので、関連テーブルにレコードが存在する主テーブルのレコードのみに絞り込まれます。
複数のレビューがあると本が重複して表示されることになるので、一意にする場合には Book.joins(:reviews).distinctにします。
JOINしたテーブルのレコードはキャッシュされません。
そのため、JOINして条件を絞り込みたいけど、JOINしたテーブルのレコードは必要ないという時に使います。
# レビューIDが1の本だけに絞り込む
Book.joins(:reviews).where(reviews: { id: 1 })
# SELECT "books".* FROM "books" INNER JOIN "reviews" ON "reviews"."book_id" = "books"."id" WHERE "reviews"."id" = 1
# 集計での使用例
# レビューがある本の数をカウント
Book.joins(:reviews).group(:id).count
2. preload
Book.preload(:author)
# SELECT "books".* FROM "books";
# SELECT "authors".* FROM "authors" WHERE "authors"."id" IN (1, 2, 3, ...)
preloadは複数のクエリに分けてレコードをキャッシュします。
複数のテーブルを指定するときや、データ量が大きいテーブルをキャッシュしたいときには preload を使うのが良さそうです。
ただし、JOINをしていないので preload で指定したテーブルのカラムで絞り込もうとすると例外が発生します。
Book.preload(:author).where(author: { id: 1 })
# SELECT "books".* FROM "books" WHERE "author"."id" = 1;
# Mysql2::Error: Unknown column 'author.id' in 'where clause'
3. eager_load
Book.eager_load(:author)
# SELECT "books"."id" AS t0_r0, "books"."title" AS t0_r1, ... FROM "books" LEFT OUTER JOIN "authors" ON "authors"."id" = "books"."author_id"
LEFT OUTER JOINを使って、指定したテーブルを一つのクエリでまとめてレコードをキャッシュします。
こちらは JOIN してるので、preloadと違って指定したテーブルでの絞り込みが可能です。
Book.eager_load(:author).where(author: { id: 1 })
# SELECT "books"."id" AS t0_r0, "books"."title" AS t0_r1, ... FROM "books" LEFT OUTER JOIN "authors" ON "authors"."id" = "books"."author_id" WHERE "authors"."id" = 1
4. includes
Book.includes(:author)
# SELECT "books".* FROM "books"
# SELECT "authors".* FROM "authors" WHERE "authors"."id" IN (1, 2, 3, ...)
Book.includes(:author).where(authors: { id: 1 })
# SELECT "books"."id" AS t0_r0, "books"."title" AS t0_r1, ... FROM "books" LEFT OUTER JOIN "authors" ON "authors"."id" = "books"."author_id" WHERE "authors"."id" = 1
単純に関連テーブルを指定した場合は preload と同じ挙動になります。
下記のようなケースでは eager_load と同じ挙動になります。
- 関連テーブルのカラムで
WHEREやORDER BYを指定している -
referencesやjoinsを呼び出している
なお、where を使って関連テーブルのカラムでの絞り込みを行う場合には注意が必要です。
SQL 文字列で条件指定をするとエラーになるため、ハッシュで渡すか references を使用する必要があります。
# エラーになる
Book.includes(:author).where('authors.name = ?', 'Alice')
# referencesを使う(OK)
Book.includes(:author).where('authors.name = ?', 'Alice').references(:author)
# ハッシュで渡す(OK)
Book.includes(:author).where(authors: { name: "Alice" })
参考