DEV Community

Prathamesh Sonpatki
Prathamesh Sonpatki

Posted on • Originally published at prathamesh.tech on

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
Enter fullscreen mode Exit fullscreen mode

Let's say I want to fetch all articles of an author, published or otherwise.

author = Author.find_by(email: "prathamesh@example.com")
author.articles.unscoped
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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]
Enter fullscreen mode Exit fullscreen mode

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'",
Enter fullscreen mode Exit fullscreen mode

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" = ?
Enter fullscreen mode Exit fullscreen mode

So next time you are using unscoped in your code, make sure you are not calling accidentally on an association.

Pro Tip

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. I used it to debug the code in this blog post.

Top comments (0)