How to (not) use unscoped in Rails
Active Record provides unscoped
to remove all the scopes added to a model previously.
class Article
default_scope { where(published: true) }
end
Article.all # SELECT * FROM articles WHERE published = true
Article.unscoped # SELECT * FROM articles
Let's say I want to fetch all articles of an author, published or otherwise.
author = Author.find_by(email: "[email protected]")
author.articles.unscoped
I am expecting all articles to be returned for given author. But there is a surprise, instead of getting articles of a specific author, I get back all articles in the database!
author.articles.unscoped
# SELECT "articles".* FROM "articles"
How did this happen?
When unscoped
is called on a model, it calls unscoped and returns a scope of that model without any previous scopes.
But we called unscoped
on an association. Let's see where the unscoped
method is defined on an association.
[5] pry(main)> author.articles.method(:unscoped)
=> #<Method: Article::ActiveRecord_Associations_CollectionProxy#unscoped>
[6] pry(main)> author.articles.method(:unscoped).source_location
=> ["/Users/prathamesh/.rbenv/versions/2.6.3/lib/ruby/gems/2.6.0/gems/activerecord-5.2.3/lib/active_record/relation/delegation.rb",
96]
Turns out, we don't have a unscoped
method defined on the association object. Instead it just delegates to the unscoped
on the model of the association through various intermediate objects.
The full stack trace of these intermediate calls is as follows:
"/Users/prathamesh/.rbenv/versions/2.6.3/lib/ruby/gems/2.6.0/gems/activerecord-5.2.3/lib/active_record/scoping/default.rb:34:in `unscoped'",
"/Users/prathamesh/.rbenv/versions/2.6.3/lib/ruby/gems/2.6.0/gems/activerecord-5.2.3/lib/active_record/associations/association_scope.rb:24:in `scope'",
"/Users/prathamesh/.rbenv/versions/2.6.3/lib/ruby/gems/2.6.0/gems/activerecord-5.2.3/lib/active_record/associations/association_scope.rb:7:in `scope'",
"/Users/prathamesh/.rbenv/versions/2.6.3/lib/ruby/gems/2.6.0/gems/activerecord-5.2.3/lib/active_record/associations/association.rb:90:in `association_scope'",
"/Users/prathamesh/.rbenv/versions/2.6.3/lib/ruby/gems/2.6.0/gems/activerecord-5.2.3/lib/active_record/associations/association.rb:79:in `scope'",
"/Users/prathamesh/.rbenv/versions/2.6.3/lib/ruby/gems/2.6.0/gems/activerecord-5.2.3/lib/active_record/associations/collection_association.rb:287:in `scope'",
"/Users/prathamesh/.rbenv/versions/2.6.3/lib/ruby/gems/2.6.0/gems/activerecord-5.2.3/lib/active_record/associations/collection_proxy.rb:932:in `scope'",
"/Users/prathamesh/.rbenv/versions/2.6.3/lib/ruby/gems/2.6.0/gems/activerecord-5.2.3/lib/active_record/associations/collection_proxy.rb:1104:in `scoping'",
"/Users/prathamesh/.rbenv/versions/2.6.3/lib/ruby/gems/2.6.0/gems/activerecord-5.2.3/lib/active_record/relation/delegation.rb:114:in `method_missing'",
"/Users/prathamesh/.rbenv/versions/2.6.3/lib/ruby/gems/2.6.0/gems/activerecord-5.2.3/lib/active_record/scoping/default.rb:34:in `unscoped'",
"/Users/prathamesh/.rbenv/versions/2.6.3/lib/ruby/gems/2.6.0/gems/activerecord-5.2.3/lib/active_record/relation/delegation.rb:114:in `public_send'",
"/Users/prathamesh/.rbenv/versions/2.6.3/lib/ruby/gems/2.6.0/gems/activerecord-5.2.3/lib/active_record/relation/delegation.rb:114:in `block in method_missing'",
"/Users/prathamesh/.rbenv/versions/2.6.3/lib/ruby/gems/2.6.0/gems/activerecord-5.2.3/lib/active_record/relation.rb:281:in `scoping'",
"/Users/prathamesh/.rbenv/versions/2.6.3/lib/ruby/gems/2.6.0/gems/activerecord-5.2.3/lib/active_record/associations/collection_proxy.rb:1104:in `scoping'",
"/Users/prathamesh/.rbenv/versions/2.6.3/lib/ruby/gems/2.6.0/gems/activerecord-5.2.3/lib/active_record/relation/delegation.rb:114:in `method_missing'",
When we are calling unscoped
on an association, it is actually getting translated to calling unscoped
on the model itself. So author.articles.unscoped
gets translated to Article.unscoped
. That's why we get all the articles without the author constraint back.
This behavior stumped me as I was expecting it to "just work" on associations as well.
If we want to unscoped
articles of an author, we need to fetch them via articles, not via author.
Article.unscoped.where(author: author)
# SELECT "articles".* FROM "articles" WHERE "articles"."author_id" = ?
We can also use block form of this method. All queries inside the block will not use the previously set scopes on the model on which unscoped
is called.
Article.unscoped { author.articles }
So next time you are using unscoped
in your code, make sure you are not calling accidentally on an association.
Pro Tip(s)
Ruby has a secret weapon to debug any program. It is a method named method
. You can call it on any object and pass the method name to get the method object. Once the method object is caught, we can ask its source location.
Ruby provides a method caller
which returns the stack trace when we use it in console with binding.pry
.
I used these two tricks to debug the code in this blog post.