More than 1 year has passed since last update.

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

スクリーンショット 2015-10-16 10.53.56.png

"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