On Default Scopes in Rails

Jon Sully

3 Minutes

They're not always bad. Just.. usually. :)

There are plenty of articles out there that will tell you to never use default_scopes. Lots of them. Most folks agree: ‘default_scopes are bad.’ There’s good reason for this mindset — notably two parts that will cause massive headaches for you in the future if you use a default_scope today:

  • Trying to retrieve records cut out by the scope is notoriously hard and dangerous
  • Default scopes impose defaults onto new model instances

I’m not saying those aren’t true. Those are definitely true. Using default scopes in the wrong way will present pain.

BUT, I believe there is exactly one decent use case for default scopes in Rails applications that folks don’t recognize. The case where the app isn’t aware of them. That is, when there is no application- or business-logic utilizing the scope for functionality.

I’m going to use soft-deleting as my example case here since it’s a well-known feature and can be used in a number of different ways. The key distinction here is whether or not the application is leveraging soft-deletes for application or business-logic functionality, so let’s illustrate that in two ways. In Situation 1, consider a Blog application that uses soft-deletes as a feature to allow authors to have a “Blog Post Trash” that holds the posts until they confirm they really want to delete them. In this case, soft-deleting is being used for application-level functionality: the soft-delete is the mechanism that directly powers the “is it in the trash?” query. Contrast that with Situation 2 and perhaps a Library application that keeps a record of who-checked-out-what-books. In this situation users can delete records from their history, but as far as the user (and the application) is concerned, once it’s deleted it’s deleted. The only reason soft-deletes exist in this situation is for auditing or data-retention purposes outside of the application.

The key difference here is whether or not the application is aware of, and attempts to make functional use of, the records on the “other side” of the default scope. In the former, the soft-delete is very much part of the user-functionality. Using a default_scope in that case would be a bad, painful idea. However in the latter, the application has no idea there are records that are soft deleted — it believes that once #destroy is called, that record no longer exists. The soft-deleted records are purely for administrative intervention, data-retention, and/or developer needs — the Rails app itself isn’t attempting to use soft-deleted records in any way.

If the soft-deleted records are not leveraged for programmatic restores/revives anywhere or used for functional purposes within the app, and serves purely as a data retention mechanism to only be leveraged by Rails developers in a production console (or manual database access, etc.), then a default scope is okay.

Now, it’s worth pointing out that what I’m suggesting as being “okay with default scopes” isn’t actually used by lots of folks (as far as I know) — it’s much, much more common to have application-functionality-based soft-deleting. For the same reasons that Situation 1‘s 'trash bin’ is helpful to end-users. But my last note here is actually about naming. If you’re working with application-functionality-level soft-deletes or other “maybe this could be a default scope?” type situations, first, don’t use a default scope. Second, name your field better! If you have a user-level feature for your User’s blog posts to be in a trash bin, name the attribute/column ruby>:user_trashed_at. If the goal is to hide content from users, name your field ruby>user_hid_at etc. etc. Application-functionality-level features should be named well with regard to the application’s domain. That’s not soft-deleting, that’s a user-feature. Name it as such 😜

EDIT: One last thing — if you plan on using default scopes (for any reason), I’d encourage you to add a second scope which negates the default scope for ease of access the next time you’re in a Rails console 😉. #unscope() does wonders here:

class ExampleClass < ApplicationRecord
  default_scope { where(deleted_at: nil) }
  scope :only_deleted_records, -> { unscope(where: :deleted_at).where.not(deleted_at: nil) }
end

Comments? Thoughts?

Please note: spam comments happen a lot. All submitted comments are run through OpenAI to detect and block spam.