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.