3
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 ActiveRecord の関連付けメソッドまとめ(joins / preload / eager_load / includes の違い)

Posted at

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 と同じ挙動になります。

  • 関連テーブルのカラムで WHEREORDER BY を指定している
  • referencesjoins を呼び出している

なお、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" })

参考

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