Madogiwa Blog

主に技術系の学習メモに使っていきます。

Ruby on Rails: BulletからRails標準のstrict loadingに乗り換えるMEMO📝

今までN+1の検知にBulletを使っていたのですが🔫

github.com

Rails 6.1から導入された新機能strict loadingでも同じようなことが実現できそうだったので、個人のWebサービスをBulletからstrict loadingに載せ替える手順をメモしておきます📝

strict loadingの概要は以前まとめているので、興味がある人はこちらからどうぞ

madogiwa0124.hatenablog.com

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にマージされている実装をもってきたものです🚃

github.com

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ブロックで使うのが最適です。

Rails アプリケーションを設定する - Railsガイド

おわりに

単純にN+1を検知するだけだったらRails標準機能の範囲でお手軽に実現出来るのは便利ですね✨

追記:2021/04/05

N+1が発生したときだけraiseするn_plus_one_onlyオプションが追加される(まだリリース予定は未定っぽい)ようなのでこちらを利用すると、より実用的な感じになりそうですね✨

github.com

参考

github.com

stackoverflow.com