今までN+1の検知にBulletを使っていたのですが🔫
Rails 6.1から導入された新機能strict loadingでも同じようなことが実現できそうだったので、個人のWebサービスをBulletからstrict loadingに載せ替える手順をメモしておきます📝
strict loadingの概要は以前まとめているので、興味がある人はこちらからどうぞ
strict_loadingをグローバルに導入する
今回はBulletの代用としてアプリケーション全体でN+1の検知を有効にしたかったので、ApplicationRecoredのstrict_loading_by_default
をtrueに設定し、ApplicationRecordを継承したModelではstrict loadingによるN+1の検知を有効にしました。
class ApplicationRecord < ActiveRecord::Base self.strict_loading_by_default = true end
これでeager_loadingをせずに関連モデルを取得しようとした際にActiveRecord::StrictLoadingViolationError
が発生するようになりました🔫👋
strict loadingを無効化する
ActiveRecordのインスタンスにstrict_loadingを無効化するメソッドを追加
先程の手順でstrict loadingによるN+1の検知が可能になったのですが、以下のような作成したインスタンスをそのまま参照して関連するmodelのインスタンスを取得したいケース等でもstrict loadingによるN+1の検知によりActiveRecord::StrictLoadingViolationError
が発生してしまいます。
describe '#foo' do let(:foo) { create(:foo) } before { create(:bar, foo: foo) } # eager_loadingせずに参照するためActiveRecord::StrictLoadingViolationErrorが発生する。 it { expect(foo.bars.length).to eq 1 } end
しかし、現在(2021/02/27)のstableの最新Rails 6.1.3ではActiveRecordのインスタンスに対してstrict loadingを有効にするstrict_loading!
メソッドは実装されていますが、無効にすることはできません😢
Foo.preload(:bars).find(foo)
のようにeagar_loadingして取得し直すような対応も考えられますが、そもそも1回アクセスするだけだったら再取得する分、かえってSQLの発行回数が増えてしまう恐れがあるのと、既存のテストコードに与える影響が大きいので、N+1を許容したいケースもあるのかなと思います。
今回は以下のようなメソッドをApplicationRecordに追加してstrict loadingを無効化出来るようにしました。
class ApplicationRecord < ActiveRecord::Base self.strict_loading_by_default = true def strict_loading!(value = true) @strict_loading = value end end
上記実装はrailsのmasterにマージされている実装をもってきたものです🚃
FactoryBotで作成したインスタンスはstrict_loadingを無効化する
先程紹介したメソッドを以下のように使ってあげるとActiveRecord::StrictLoadingViolationError
を回避することができます。
describe '#foo' do let(:foo) { create(:foo) } before do foo.strict_loading!(false) create(:bar, foo: foo) end it { expect(foo.bars.length).to eq 1 } end
FactoryBotのglobalなtraitを定義して以下のようにするもの良さそうですね🤖
FactoryBot.define do trait :strict_loaded do after(:build) { |record| record.strict_loading!(false) } end end describe '#foo' do let(:foo) { create(:foo, :strict_loaded) } before do create(:bar, foo: foo) end it { expect(foo.bars.length).to eq 1 } end
または以下のような形でfactory内でtraitをデフォルトで呼び出すこともできそうです👀
FactoryBot.define do trait :strict_loaded do after(:build) { |record| record.strict_loading!(false) } end factory :foo do strict_loaded end end describe '#foo' do let(:foo) { create(:foo) } before do create(:bar, foo: foo) end it { expect(foo.bars.length).to eq 1 } end
console時にstrict loadingを無効化する
console時にも毎回strict_loading!(false)
を実行するのは面倒なので、application.rb内で以下のように記述するとconsole時のみstrict loadingを無効化することができます。
# config/application.rb module MyApp class Application < Rails::Application # rails consoleではstrict loadingを無効化 console { ApplicationRecord.strict_loading_by_default = false } end end
config.console
の説明は以下を参照してください。
config.console: これを用いて、コンソールでrails consoleを実行する時に使われるクラスをカスタマイズできます。このメソッドはconsoleブロックで使うのが最適です。
おわりに
単純にN+1を検知するだけだったらRails標準機能の範囲でお手軽に実現出来るのは便利ですね✨
追記:2021/04/05
N+1が発生したときだけraiseするn_plus_one_only
オプションが追加される(まだリリース予定は未定っぽい)ようなのでこちらを利用すると、より実用的な感じになりそうですね✨