default_scope: every query, one line, no exceptions
One line that rewrites every query on a model
Every ActiveRecord querying course so far filtered explicitly, with .where(status: 0) or Catalog::Course.active: one call, applied when a query chose to apply it. default_scope is a different kind of tool. It's a condition declared once, in the model, and folded into every query against that model automatically, whether .all, .count or .first, with nothing at the call site asking for it.
The condition nobody typed
A scratch table with an active column and a default_scope declared once:
class ScratchDsCourse < ActiveRecord::Base
default_scope { where(active: true) }
end
ScratchDsCourse.create!(title: "Visible", active: true)
ScratchDsCourse.create!(title: "Hidden", active: false)
ScratchDsCourse.count # => 1, not 2
ScratchDsCourse.all.pluck(:title) # => ["Visible"]
Both rows exist; both inserts succeeded. Yet .count and .all, called with no .where in sight, both leave out the inactive one. Every later query against ScratchDsCourse, from any file, carries WHERE active = TRUE, whether or not the code asking knew the condition existed.
This app's choice: no default_scope anywhere
No model in this app uses default_scope. Catalog::Course, the closest parallel to ScratchDsCourse, has an active column and hides inactive courses from the public roadmap, the shape default_scope would handle automatically. Its implementation:
# app/models/catalog/course.rb
scope :active, -> { where(active: true) }
That's an explicit, named scope, not a default one. Catalog::Course.all returns every course, active or not. Filtering to active courses is a deliberate .active call, as in the line Catalog::Course.active.ordered... in Catalog::ListCoursesQuery, not something baked into every query whether the caller wants it or not.
Why that choice
Admin::Catalog::ListCoursesQuery has to show an admin every course, inactive ones included; that's the point of an admin listing. It never calls .active, because Catalog::Course already returns everything. If Catalog::Course used default_scope { where(active: true) }, that admin query would have to break out of the default on every call, with .unscoped or .unscope(where: :active) (the next two lessons), just to see the inactive courses it exists to manage. The explicit scope :active makes the public case (only active courses) one word away, .active. The admin case (everything) needs nothing extra, because the default is "no filter", which is what .all has always meant.
Key takeaways
- With
default_scope { where(...) }, the condition applies to every query against the model automatically,.all,.count,.first, with nothing at the call site asking for it. - No model in this app uses
default_scope.Catalog::Coursefilters to active courses with an explicitscope :active, -> { where(active: true) }. Catalog::Course.allreturns every course, active or not. Filtering happens only when.activeis called, asListCoursesQuerydoes.- The reason:
Admin::Catalog::ListCoursesQueryneeds every course, unfiltered. An explicit scope makes that the free case and "only active" a one-word opt-in, the reverse of what adefault_scopeonactivewould force.
Keep going with lesson 2
The find(id) trap
default_scope also filters find(id), which raises RecordNotFound for a real row that fails the scope's condition.
A free account also gives you the quizzes, your progress and notes, and the discussion.