TL;DR

Active Record’s default_scope should be renamed to something like always_prepend_scope to better describe what the method does.

The Inspiration

The other day, “default_scope” is an anti-pattern came across my twitter feed.  Having been burned by this issue I get the sentiment.

The Problem with default_scope

Let’s say I have a simple class like the following (stolen from the default_scope documentation)

Now let’s look at some sample ruby calls and the SQL they generate

When I run Article.all the SQL executed is SELECT “articles”.* FROM “articles” WHERE “articles”.”published” = ‘t’.  Great, the default scope has been used and I only see published articles.

But let’s say now I want to find all articles with the title ‘a’ REGARDLESS of their published status.  My initial thought was that Article.where(title: “a”) would work because my where would override the default where.  But as we can see, the SQL is now SELECT “articles”.* FROM “articles” WHERE “articles”.”published” = ‘t’ AND “articles”.”title” = ‘a’.  The where statement that I added does not override the where of the default scope, it appends it.  OK, I find that to be unexpected, but to be fair, the documentation says:

    ‘Use this macro in your model to set a default scope for all operations on the model’

So punish me with 50 lashes with a wet noodle for not reading documentation.  But noodle flagellation (aside: I hope this blog post eventually ranks high in searching for that phrase) doesn’t write code.  So how do I get around the default_scope.  This is the real problem is that it is REALLY HARD AND UGLY to now form an arel statement that removes the default_scope.  The best way I have found to do it is to call the protected, thinly documented, method with_exclusive_scope from an instance_eval block on an ActiveRecord::Base object (to get around the protected part).  BLAH!!!

An Anti-Pattern?

In my mind, this fits right into the Anti-Pattern criteria.  At first blush, the functionality seems useful but as time unfolds, it leads to ugliness and bad consequences.

But the real problem is not with the functionality, rather that default_scope is deceptively named. 

The Webster’s definition for default:

     a : a selection made usually automatically or without active consideration due to lack of a viable alternative <remained the club’s president by default> b : a selection automatically used by a computer program in the absence of a choice made by the user

So in my mind, when I set the default scope it is the scope that should be used if I do not make an explicit choice.  But once I make an explicit choice, like when I say where(title: “a”) then the default should NO LONGER be used.

Because of the inappropriate naming, the developer, when he first uses default_scope, does not realize what he is setting himself up for. Perhaps renaming the method to something like always_prepend_scope would make developers realize what is happening and make their code easier to manage.

UPDATED: Discussion

Chap Ambrose brought up using unscoped.  I’ve tried that in the past, you need to remember to use it in a block Article.unscoped{Article.where(title: ‘a’)} rather than just straight up Article.unscoped, which I would always screw up.  Secondly, I did not see unscoped out on http://api.rubyonrails.org so I wasn’t sure if that was still appropriate.  Finally, I still think it holds up as an anti-pattern as you initially think the default_scope will make your life easier, but it instead leads to having to put your arel in these unscoped blocks.  Still ugly.