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