Why is Ruby on Rails' default_scope bad?
A pretty popular opinion in the Ruby on Rails community is that default_scopes should be avoided at all costs. Even though this is a widely expressed opinion, often at the time it’s hard for even Rails experts to express exactly how default_scopes are bad.
The reason is two-fold:
- When you request
Model.all, it is right to assume you will be retrieving all records default_scopedoes not always cooperate with other clauses
Using default_scopes
Starting with a very basic Post model that contains a deleted_at column to
support soft delete, like so:
class CreatePosts < ActiveRecord::Migration[6.0]
def change
create_table :posts do |t|
t.string :title
t.text :body
t.datetime :deleted_at
t.timestamps
end
end
endThe application is supposed to only display posts that have not been deleted,
so a default_scope seems like the perfect solution!
class Post < ApplicationRecord
default_scope -> { where(deleted_at: nil) }
endThis default_scope successfully hides all posts that are deleted, which is great!
Additionally, the posts should be ordered by the date they were created, which
can also be added to the default_scope.
class Post < ApplicationRecord
default_scope -> { where(deleted_at: nil).order(:created_at) }
endProblem 1
Now that the application hide the proper posts by default, a feature request comes in for administrators to show deleted posts.
This is super easy with a scope, like so:
class Post < ApplicationRecord
default_scope -> { where(deleted_at: nil) }
scope :deleted, -> { where.not(deleted_at: nil) }
endThe issue occurs when this scope is used by calling Post.deleted, no
results are returned, even when there are indeed deleted records.
This is caused by conflicting where clauses, where the default_scope
requests for all records where posts.deleted_at IS NULL and the scope
requests all records where posts.deleted_at IS NOT NULL, which will always
be exactly 0 records.
Adding unscoped to the deleted scope kind of works:
class Post < ApplicationRecord
default_scope -> { where(deleted_at: nil) }
scope :deleted, -> { unscoped.where.not(deleted_at: nil) }
endHowever, this now makes it impossible to build a where clause like so:
Post.where(title: 'My Title').deletedThis is caused by deleted effectively unscopeing all of the where clauses
prior, which removes the where clause, displaying all deleted records.
Problem 2
If we want to order the posts by updated_at, we would
typically use the following code:
Post.order(:updated_at)With the default_scope applied, this results in being ordered by created_at first, then updated_at, deeming this order useless…
Instead, it must be remembered to reorder, which is not intuitive when debugging:
Post.reorder(:updated_at)Problem 3
Finally, Post.all no longer returns all records, which is misleading.
Solution
In Rails, default_scopes are always painful to work with and result in unexpected bugs, so it’s best to not use Rails’ default_scopes.
Instead, communicative scopes can be used:
class Post < ApplicationRecord
scope :active, -> { where(deleted_at: nil) }
scope :deleted, -> { where.not(deleted_at: nil) }
endAs far as the order clause, I typically just type that one out. It’s short,
sweet, and communicates the order within the line of code you’re using. Often I
combine these scopes to make a scope like for_index in which specifies what
should be shown, and in what order, like so:
class Post < ApplicationRecord
scope :active, -> { where(deleted_at: nil) }
scope :deleted, -> { where.not(deleted_at: nil) }
scope :for_index, -> { active.order(:created_at) }
endEven though it seems like more work to include these scopes almost every time you access the records, it ends up being less work as the app ages. In Rails, default_scopes should be avoided at all costs. You’ll regret using default_scopes, maybe not today, but some time in the future.
Related Articles
- Solved: Warning: Using the last argument as keyword parameters is deprecated
- Rails: How to Use Greater Than/Less Than in Active Record where Statements
- How to Open a New Tab Vim
- Solved: ActionController::ParameterMissing (param is missing or the value is empty)
- Vim: How to Open Current Opened File in New Tab