ActiveRecord::Base.unscope の呼び出しでネストが深くなっているのを浅くする

仕事で見かけたコードを汎用化した内容にして、個人ブログにメモっておく。*1

多段ネストを reverse して Proc でラップしたり、それを RSpec で検証する方法はブログ書いてないと忘れる自信がある。

モデルの定義

class User < ApplicationRecord
  default_scope { where(deleted_at: nil) }
end

class Article < ApplicationRecord
  default_scope { where.not(published_at: nil) }
end

元コード

User.unscoped do
  Article.unscoped do
      assert_equal 'SELECT "users".* FROM "users"', User.all.to_sql
      assert_equal 'SELECT "articles".* FROM "articles"', Article.all.to_sql
  end
end

書き換えたあと

module ScopeHelper
  def self.unscoped(models:, &block)
    models.reverse
      .inject(block) { |result, model| -> { model.unscoped(&result) } }
      .call
  end
end

ScopeHelper.unscoped(models: [User, Article]) do
  assert_equal 'SELECT "users".* FROM "users"', User.all.to_sql
  assert_equal 'SELECT "articles".* FROM "articles"', Article.all.to_sql
end

RSpecで書いたテスト

RSpec.describe ScopeHelper do
  describe '.unscoped' do
    it do
      expect(User).to receive(:unscoped).and_call_original
      block = -> {}
      expect(Article).to receive(:unscoped) do |&args|
        expect(args).to eq block
      end

      ScopeHelper.unscoped(models: [User, Article], &block)
    end
  end
end

*1:default_scopeとか絶対使わないけど、他に汎用化した例は思い浮かばなかった