A way to skip default scopes?

Default scopes are weird, I mean, the feature itself makes sense, a lot, but if you think on applications where you have global administrators that have access to all everything, including the data that is filtered by the default scope, or in a specific controller you want to order the data by a different attribute.

It would be nice if we could, somehow, selectively skip the default scope.

User.skip_default_scopes.where(email: email)

Hopefully it makes sense.

1 Like

unscoped lets you do this! It’s a little dangerous to use with associations, however, because it also removes the association-related scoping.

There are also a few methods like reselect that let you remove prior scopes in a more targeted way:

2 Likes

I always avoided the default scopes in Rails, precisely because there was no way of turning them off without resorting to potentially dangerous unscoped method that disables much more than just what I’d want. It would be amazing if default scopes actually could have a label/name so we could disable the specific ones.

As an example, soft delete immediately comes to mind. It would be amazing if I could

class Product < ActiveRecord::Base
  default_scope :not_deleted, -> { where(deleted_at: nil) }
end

# and then in admin
Product.unscoped(:not_deleted) # or different, backwards compatible name for the method
3 Likes

That’s a really interesting idea! I tend to use other mechanisms (e.g. Papertrail) for soft delete but I can see a lot of uses for making it easier to safely disable default scopes.

(I needed to debug a “fun” memory safety + multitenancy bug a few years ago; it might never have happened if easy default-scope disabling existed.)

Do you have time to work on something like this? Does anyone else reading this have that kind of time?

tend to use other mechanisms (e.g. Papertrail) for soft delete

Just out of curiosity, can you expand a little bit better how you use Papertrail for soft deletes?

Do you have time to work on something like this? Does anyone else reading this have that kind of time?

I would love to work on it

There’s also rewhere:

Post.where(active: true).where(trashed: true).rewhere(trashed: false)
# WHERE `active` = 1 AND `trashed` = 0

https://api.rubyonrails.org/classes/ActiveRecord/QueryMethods.html#method-i-rewhere

Yeah! Papertrail has a reify method that lets you restore a prior version of an object, which also goes for deleted objects. There’s a bit of logic to make sure associations get restored correctly, but not that much more than any soft-delete case.

Yay! Right now the best way to propose a new Rails feature is to think of a small, achievable version of the feature (you’ve already done that) and open an issue in the GitHub issue tracker. Make sure that you mention that you’re interested in working on the feature. Because Rails has a lot of users and not very many people working on it, the maintainers tend to not accept feature ideas unless the person proposing them is also willing to do the work of championing the idea through.

There are a bunch of re* methods that allow you to selectively adjust relations without using the hammer that is unscoped - checkout rewhere, reselect and reorder.

2 Likes

Your code example of a named global scope that you can easily disable with unscoped(:name_goes_here) is exactly what I had in mind too!

I try to avoid global scopes as well, but there are legitimate use cases for them, and thus legitimate use cases to disable them. The two use cases I have in mind are:

  • soft deletes with for example the acts_as_paranoid gem. Look at the source code of that gem, and the hacks they do to unscope their global scope to allow you to do SomeModel.with_deleted is a bit mind-bending but works. Unfortunately their hack work because their scope is very simple with a single component in their where condition. It wouldn’t work if their global scope WHERE query had an OR condition.

  • Basic multi-tenancy with a global scope (example implementation in this screencast). There are cases, for example in a migration script or background job, where you want to migrate all records and thus unscope them.

Named global scopes are a great idea IMO because unscoping them seems like it would make things easier.

Rewhere as mentioned by @Andrew_White unfortunately blows away every other global scope that used a where, so say if you have 2 or more global scopes in your app (due to gems, multi-tenancy, etc - this is the case for our app) that use a where, then you’re out of luck. EDIT: what I just said is wrong if the global scopes WHEREs are done on two different fields.

The same goes for a global unscope to a much larger extent.

I don’t think that’s true. rewhere allows you to change a single where, while keeping other scopes:

Post.where(active: true).where(trashed: true).rewhere(trashed: false)
# WHERE `active` = 1 AND `trashed` = 0

https://api.rubyonrails.org/classes/ActiveRecord/QueryMethods.html#method-i-rewhere

That’s right, my bad I posted out of bad memory.

Rewhere works well for one simple field but I believe you can’t rewhere something like where(“x = true OR y = true”), is that right? We unfortunately have a global scope like that for our multi-tenancy; I tried rewhere for this a few months ago, to no avail.

Also, default_scope can contain other things than where clauses.

I think default scopes would confuse less, if we’d simply ignore them for the all scope because “all” means “all”!

2 Likes