How to Carefully Remove a Default Scope in Rails

Step 1: Measure Test Coverage

The following technique requires high test coverage.

Step 2: Identify Call Sites

Where does the default_scope get used?

We can print a stack trace whenever the default_scope is used.


# myapp/app/models/shirt.rb
require "pp"
class Shirt
  default_scope {
    pp caller # prints a stack trace
    where(color: "red")
  }
end

Assuming high test coverage, simply run your test suite to see all the call sites.

Step 3: Focused Stack Traces

Filter out, e.g. gems by adding a `select`.


# myapp/app/models/shirt.rb
require "pp"
class Shirt
  default_scope {
    pp caller.select { |line| line.include?("myapp") }
    where(color: "red")
  }
end

Step 4: Reduce the Number of Call Sites to Zero

Example 1: A Simple Query


Shirt.all # finds red shirts

Use `unscoped`.


class Shirt
  def self.red
    where(color: "red")
  end
end
Shirt.unscoped.red # finds red shirts, does not use default scope

Once the default scope has been removed, the call site will continue to work and the calls to `unscoped` can be removed at your leisure.

Example 2: An Association


class Outfit
  has_many :shirts
end
Outfit.create.shirts.to_sql
# select * from shirts where shirts.color = 'red' and outfit_id = ?

In this situation we cannot use `unscoped`. Instead, use `unscope`.


class Outfit
  has_many :shirts
end
Outfit.create.shirts.unscope(where: :color).red.to_sql
# select * from shirts where outfit_id = ? and shirts.color = 'red'

Once again, after the default scope has been removed, the call site will continue to work and the call to `unscope` can be removed at your leisure.

You may notice that the order of terms in the where clause changed. The order of terms has no effect in SQL.

Example 3: A Constructor


shirt = Shirt.new
shirt.color # => "red"

Use `unscoped` at the call site:


shirt = Shirt.unscoped.new(color: "red")
shirt.color # => "red"

If you find yourself repeating default values in the constructor, don’t. How about an alternate constructor?


class Shirt
  def self.new_with_default_values
    new(color: "red")
  end
end

Step 5: Remove the Default Scope

Celebrate by eating a plate of spaghetti.

Step 6: Remove the Calls to `unscoped` and `unscope`

They should be easy to search for.

Step 7: Disable default_scope


# config/initializers/disable_default_scope.rb
module ActiveRecord::Scoping::Default::ClassMethods
  def default_scope
    fail "Don't use default_scope"
  end
end