eager_load
はJOINしてキャッシュもしてくれるActiveRecordのメソッドだが、この仕様を正しくイメージできてなかったのではまった.
has_many
に対してIN句検索したときに一部のhas_many
データのみがキャッシュされて意図しない結果となった.
joins
ではキャッシュされないためこの問題は起こらないが、当然キャッシュされないので後から追加クエリが発行される.
includes
は条件によってeager_load
と同じ動きをするため今回の用途だと同じ問題が発生する.
ActiveRecordのjoinsとpreloadとincludesとeager_loadの違い
再現
超簡単なhas_many
関連を作成する.
class User < ActiveRecord::Base
has_many :roles
end
class Role < ActiveRecord::Base
belongs_to :user
end
user = User.create(name: "test")
# id=17
user.roles.build(name: "programmer").save
user.roles.build(name: "designer").save
user.roles.build(name: "infra").save
"programmer","designer","infra"をひもづけたのでuser.roles
は当然3件とれてほしい
User.find(17).roles.length
# => 3
しかしprogrammerを含むuser一覧を取得すると、rolesの件数がおかしくなる. これはjoinでマッチした一部のデータのみがキャッシュされてしまうため.
User.eager_load(:roles).where(roles: { name: ["programmer"] }).find(17).roles.length
# => 1
User.eager_load(:roles).where(roles: { name: ["programmer","designer"] }).find(17).roles.length
# => 2
User.eager_load(:roles).where(roles: { name: ["programmer","designer","infra"] }).find(17).roles.length
# => 3
前述のとおりincludes
でも同じ問題が起きる.
User.includes(:roles).where(roles: { name: ["programmer"] }).find(17).roles.length
# => 1
解決策を考えてみたけど、キャッシュさせないまたはキャッシュを捨てるしか思いつかない.
追記:コメント欄にキャッシュもしつつすべてのhas_many
を取得する解決策を記載いただきました。@jnchitoさんありがとうございます。
User.joins(:roles).where(roles: { name: ["programmer"] }).find(17).roles.length
# => 3
# joinsはキャッシュしないためこの問題発生しない
User.includes(:roles).where(roles: { name: ["programmer"] }).find(17).roles(true).length
# => 3
# roles(true)とするとforce_reloadになるのでキャッシュを捨てて強制的にSQL発行される
References
Environment
% rails -v
Rails 4.1.9