前提
- 「カード」と「カードセット」というモデルがあるとする
- 「カードセット」に複数の「カード」が紐づいているとする
- 「カード」には「remembered」属性がある
- あるページで各カードセットのカードの総数や「remembered」の true の数をカウントする
- したがって、単純に実装するとN+1が発生する
サンプルコード
# == Schema Information # # Table name: cards # # id :bigint not null, primary key # remembered :boolean not null # card_set_id :bigint not null # created_at :datetime not null # updated_at :datetime not null # class Card < ApplicationRecord belongs_to :card_set end # == Schema Information # # Table name: card_sets # # id :bigint not null, primary key # name :string not null # created_at :datetime not null # updated_at :datetime not null # class CardSet < ApplicationRecord has_many :cards end
メモ
コントローラーで以下のように実装すると(1)(2)両方で、クエリが発行されてN+1状態になってしまう。
def test @card_sets = CardSet.all @card_sets.each do |card_set| card_set.cards.size # (1) card_set.cards.where(remembered: true).size # (2) end end
そこで以下のようにカードセットを取得するときに eager_load
すると上記の (1) でキャッシュが効き、クエリが発行されなくなる。
@card_sets = CardSet.all.eager_load(:cards)
しかし、(2)ではまだクエリが都度発行されてしまう。これは where メソッドが都度SQLを発行してしまうことによる。Ruby上では各カードの関連データをキャッシュしているので、 (2) を以下のようにRuby上で計算するように変更する。
card_set.cards.select(&:remembered).size
するとクエリの発行が無くなりN+1を回避できる。
さらにメモ
preload は単純にSQLが2回発行される
CardSet.where(user_id: user_id).preload(:cards)
SELECT "card_sets".* FROM "card_sets" WHERE "card_sets"."user_id" = $1 LIMIT $2 OFFSET $3 [["user_id", 1], ["LIMIT", 10], ["OFFSET", 0]] SELECT "cards".* FROM "cards" WHERE "cards"."card_set_id" IN ($1, $2, $3) [["card_set_id", 1], ["card_set_id", 2], ["card_set_id", 3]]
eager_load は LEFTER JOIN するテーブルがキャッシュされる
CardSet.where(user_id: user_id).eager_load(:cards)
SELECT DISTINCT "card_sets"."id" FROM "card_sets" LEFT OUTER JOIN "cards" ON "cards"."card_set_id" = "card_sets"."id" WHERE "card_sets"."user_id" = $1 LIMIT $2 OFFSET $3 [["user_id", 1], ["LIMIT", 10], ["OFFSET", 0]] SELECT "card_sets"."id" AS t0_r0, "card_sets"."public" AS t0_r1, "card_sets"."forked" AS t0_r2, "card_sets"."name" AS t0_r3, "card_sets"."user_id" AS t0_r4, "card_sets"."created_at" AS t0_r5, "card_sets"."updated_at" AS t0_r6, "cards"."id" AS t1_r0, "cards"."answer" AS t1_r1, "cards"."question" AS t1_r2, "cards"."hint" AS t1_r3, "cards"."remembered" AS t1_r4, "cards"."card_set_id" AS t1_r5, "cards"."created_at" AS t1_r6, "cards"."updated_at" AS t1_r7 FROM "card_sets" LEFT OUTER JOIN "cards" ON "cards"."card_set_id" = "card_sets"."id" WHERE "card_sets"."user_id" = $1 AND "card_sets"."id" IN ($2, $3, $4)