Hisakeyのブログ

エンジニアが色々呟くブログです。

reloadメソッドに沼りました。

はじめに

Rails のコントローラーで User に紐づく全てのアソシエーションを preload して、N+1 を解消しようとしました。

user = User.preload(:posts, :comments).find(params[:id])

SQL のログを確認すると、 preload によって関連データがまとめて取得されていることを確認できました。

しかし、実際には後続でクエリが発行されており、無駄なクエリ発行を抑えられていませんでした。

原因

ログをよく見ると、意図しないタイミングで postscomments に対する追加のクエリが発行されていました。

あるクラスのメソッド内で user.reload が呼ばれていることをわかりました。

class SomeService
  def initialize(user)
    @user = user
  end

  def execute
    @user.reload  # ← ここが問題
    # ここで @user.posts や @user.comments を参照すると再クエリが発行される
  end
end

reload を実行すると、すでに preload されていたデータがリセットされ、次回アクセス時に再度クエリが発行されてしまうことが判明しました。

reload とは?

Rails の公式ドキュメントを調べてみると、reload メソッドは以下のように説明されています。

apidock.com

Attributes are reloaded from the database, and caches busted, in particular the associations cache and the QueryCache.

一度取得したオブジェクトのキャッシュがクリアされ、データベースから最新の情報が再取得されるのです。

そのため、preload していたデータもリセットされてしまい、再度 user.postsuser.comments にアクセスすると、新しくクエリが発行されることになります。

まとめ

今回のケースでは、単純に reload を削除することで、不要なクエリを防ぐことが可能になります。

reload は、データの整合性を担保したいときには有効ですが、N+1 の回避を目的としているときに不用意に使うと、逆にクエリが増えてしまうことがあります。

reloadメソッドを使用するときは、本当に必要なのかどうかを吟味することが必要と勉強になりました。