Superhero

What Are Named Scopes?

Of all the great new features of Rails 2.1 (and the forthcoming Rails 2.2), named_scope is my favorite. named_scope provides a mechanism for wrapping up a set of find conditions. Let's check out an example to see how this all works.

Imagine we have a Person model with the following schema:

create_table :people do |t|
  t.string :eye_color
  t.integer :age
  t.boolean :admin
end

Now, in many of our finds we are probably going to be interested in that :admin attribute, especially when we're looking up users that are able to log into the admin section of our site. Normally, we might write a find something like this:

@admins = Person.find(:all, :conditions => {:admin => true})

That would grab all the people in our database who are admins. However, that find syntax is a little verbose, especially for a controller (which is probably where we might find code like this). named_scope to the rescue! We'll add the following to our Person model:

named_scope :admins, :conditions => {:admin => true}

This allows us to change our controller call to something a bit closer semantically to what we're actually doing, leaving the meat of the logic in our model where it belongs:

@admins = Person.admins

We may also be interested in our non-admins as well, so we might write another named scope like the following:

named_scope :non_admins, :conditions => {:admin => false}

But this is starting to seem a little redundant. Our admin and non_admin named scopes are almost the same, just flipping the boolean value in our conditions. There must be a better way... and there is! Our old friend lambda to the rescue:

named_scope :admins, lambda {|boolean| {:conditions => {:admin => boolean}} }

Now we can pass the boolean value to our named_scope like so:

@admins = Person.admins(true)
@non_admins = Person.admins(false)

Nice and DRY... Like a Good Chardonnay

Now, I know what you're thinking. You're thinking you'd like a nice glass of chardonnay right now. But this article's not about chardonnay. This article's about named_scope... so stay with me. No, you're thinking, "Gee, couldn't I just write some class-level finder methods and achieve the same thing?" Of course you could. Before Rails 2.1 and named_scope that's exactly what you might have done. Something like this:

def self.admins(boolean)
  find(:all, :conditions => {:admin => boolean})
end

And that would work the same as our "admins" named scope does, with a few big exceptions. The thing that makes named_scope so powerful is the ability to chain them together to make more complex finds.

Let's say our admin area for our site is going to have an ad for that chardonnay I mentioned earlier. Naturally, we're going to want to make sure our admins are also of legal drinking age. If we weren't using named scopes, we would write another class-level finder method, something like the following:

def self.drinkin_admins
  find(:all, :conditions => ['age >= ? and admin = ?', 21, true])
end

Hmm. Is your code Spidey-sense tingling like mine is? We've got essentially the same admin conditions from our admins class-level finder mixed in there. Not so DRY. Let's see how named scopes would make this situation better:

class Person < ActiveRecord::Base
  named_scope :admins, lambda {|boolean| {:conditions => {:admin => boolean}} }
  named_scope :drinkers, :conditions => ["age >= ?", 21]
end

Our resulting find might look something like this:

@drinkin_admins = Person.admins(true).drinkers

Just like that, we've got all the admins who are also over 21 years old. But more than that, we haven't mixed our conditions together. And because our drinkers named scope is separate, we can use it in other scenarios as well.

Furthermore, what if you need to get a count of all the admins who are over 21? Before named_scope, you'd write a second class-level finder method using the count method instead of find. But because named scopes soak up all those conditions and evaluate them at the time that you need them, you can simply add a .count to the end of your scope-chain.

@drinkin_admins_count = Person.admins(true).drinkers.count

Is That a Cape and Tights I See?

Of all the awesomeness that we've seen, named_scope is just getting started. Let's say one of our scopes is "almost" exactly what we want, but we really only need the first 10 results, or maybe we want to order the results in some special way. No worries, named_scope pulls out it's super hero outfit and saves the day:

@first_ten_drinkers = Person.drinkers.scoped(:limit => 10, :order => 'eye_color')

And just like that we can scope our named_scope even more, on the fly, no problems. The scoped named scope is special in that it can get merged into the scope chain and add additional conditions right there inline.

Now, this is all well and good, but we can make this whole thing even easier. What if we added another scope called limit, like the following:

named_scope :limit, lambda {|num| {:limit => num}}

Now we can limit any find we do off the Person model simply by including the limit scope in the chain, like so:

@first_ten_drinkers = Person.drinkers.limit(10).scoped(:order => 'eye_color')

Nice! Now that's feeling a whole lot better. I bet you can think of a few others that would be helpful too, like an order scope that lets you order by created_at dates. And we probably want these scopes on all of our ActiveRecord models, right? In fact, Ryan Daigle has already done a lot of that work for us with his utility_scopes gem (http://github.com/yfactorial/utility_scopes/).

script/plugin install git://github.com/yfactorial/utility_scopes.git

Now you'll have all kinds of great named scopes to make finding records easy as pie, including:

Model.with(:association)    #=> eager loads the association, so you don't have to.
Model.except(1, 2, 3)       #=> returns all of the models except the ones with ids 1, 2 or 3
Model.limited               #=> returns the first 10 (default, no argument)
Model.limited(5)            #=> returns the first 5
Model.order_by(:eye_color)  #=> results ordered by the eye_color attribute.

And much, much more. Check out the README for utility_scopes on github.

What's that you say? Pagination? With will_paginate? Well, wouldn't you know it, will_paginate uses named_scope to implement it's pagination methods. So, just tack a "paginate" on the end of your scope-chain, like so:

@drinkin_admins = Person.admins(true).drinkers.paginate(:page => params[:page])

Easy peasy.

For more information on named_scope, check out these fine articles across the internet:




RSS Feed


CATEGORIES


ARCHIVES


BOOKMARKED


Add to Technorati Favorites