default_scope: every query, one line, no exceptions

Free preview 3 min to read

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::Course filters to active courses with an explicit scope :active, -> { where(active: true) }.
  • Catalog::Course.all returns every course, active or not. Filtering happens only when .active is called, as ListCoursesQuery does.
  • The reason: Admin::Catalog::ListCoursesQuery needs every course, unfiltered. An explicit scope makes that the free case and "only active" a one-word opt-in, the reverse of what a default_scope on active would force.
Lesson 1 of 7 · free

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.

Type to start searching...