Let’s say you have a User model. Let’s also say that you want the ability to archive your user records.
So, you add a boolean attribute called
archived that defaults to
false which can be set to
true in order to archive your user records. Now you can look for archived records by performing the following query throughout your app:
Well that’s nifty… until you realize that you’re having to write out that blasted
where() statement in five billion places throughout your application. Is that DRY? Quite the opposite, I tell you! What happens if you change that attribute’s name, you’re bound to miss at least a few instances of it in the change. For the love of all that is holy – this has just become a horrible solution!
Four solutions to this problem spring to mind: one is outright dumb, two are common and one is simply beautiful.
The first option is to put a query like this in our views.
Seriously, if you find yourself putting ActiveRecord queries in your views, just go back to PHP, please. If you’re simply new to Rails and honestly don’t know any better, read on. The second option is to put this query in your controller.
While this isn’t necessarily a bad solution, it tends to get repeated… a lot in some cases, which once again goes against the “Don’t Repeat Yourself” mantra. Another solution is to move this logic into the model. Well, well. You’re one smart cookie, you! Skinny controllers, fat models. Let’s move this clunky query into the model!
This is much prettier! But this method is gonna get all kinds of lost in any respectably sized model. This used to be the best way to store a query for frequent use until Rails 2.1 was released with named_scope in its bag of tricks. Now, in Rails 3,
named_scope has a new persona, and it’s here to… fight crime… and stuff.
scope method belongs in the list of incredibly useful model methods along with associations and validations. This method expects two arguments; either a name and a condition or a name and a block. We can easily convert our above query using the name and condition combination.
This can be used to grab all of the archived users, similar to the previous example.
Delightful! Seriously, I feel like I just ate a Hot Pocket. Oh, actually, that doesn’t feel good at all. But this scope looks great! It’s clean, on one line and behaves beautifully.
So, how would we use a block with scope? Think of any situation where you’d want to pass data into a query. This is where you’d use a block with scope. In our example, we want to return all users with whatever role we pass it.
Now we reference the scope, passing a role into it as an option.
Holy cow! So, basically we can take a massive query and slap a small scope name onto it and reference it whenever and wherever we need to. If this query changes, we change it on one place. Graceful, elegant and easy to update.
Now that you know how scopes work, I would be a horrible teacher if I didn’t introduce you to
default_scope. This beast will allow you to specify some query goodness that you want appended to *every* query you run on that model. Using our example above, what good would an
archived scope be if
User.all returned the archived users too! Not very much use in my book. We can use
default_scope to tell the model when we search normally, we only want the users who are not archived to be returned.
Now when we do a search for users, it will only ever return users who have archived set to false. Slick! But what if we want to get around that for a query or two? Let’s say we want to count ALL users, archived or not. We’d use
unscoped here. Let’s pretend we have 75 users, 5 of which have archived set to true
One more thing. You may have recognized a reoccurring bit of code throughout my examples.
We’re iterating through the list of users in the @users instance variable and outputting the name attribute of each. If you only need one attribute from each record in a list, you have just encountered one use case for Symbol#to_proc. Instead of an
each block, we could simply use the following:
This list of names can now be used however you like. Perhaps a nice join() method would meet the need?
Now, go call your mom and tell her all about the wonderful things you learned here. After that, go forth and use scopes and Symbol#to_proc like the mofo you know you are!