Agile Ajax

Named Scopes Are Awesome

named_scope.jpg

My favorite new Rails 2.1 feature is named scopes (previously available as the has_finder plugin. Scopes have already completely changed the way I write complex find logic in my ActiveRecord models.

The basic idea is simple. ActiveRecord has grown a new class method named_scope, which lets you map a set of find criteria to a name:


named_scope :active, :conditions => {:is_active => true}

At which point active is available to all instances of that ActiveRecord class, user.active.

(About the picture -- it's a periscope. With a name. Get it? Okay, you try and come up with a picture that symbolizes named scopes, especially since I just did a name tag bit a couple of weeks ago...)


So far, this is almost completely equivalent to the following class method:

def self.active
  find_by_is_active(true)
end

The scopes don't have to be conditions, but can be any find options:

 named_scope :by_name, :order => "name ASC"

So far, that's not very exciting, but here are two more named scope features. First, the scopes can be dynamic and take arguments:

 named_scope :recent, lambda { |days|   :conditions => ["date_created > ?", days.ago]}

Second, and most importantly, the scopes can be composed:

user.active.by_name user.recent.active

This is where the real power of named scopes comes in: you can split complex logic into small pieces that can be put together cleanly to make the multi-part search.

For example: ActiveRecord provides a nice shortcut for describing a search where all the criteria are based on equals -- you can enter the conditions as a hash (such as {:is_active => true, :last_name => 'rappin'}. But if that second criteria is an SQL LIKE command or something, then you're back to composing SQL conditions. Plus, I can never remember exactly how to format a LIKE command. But with named scopes, this becomes a snap. The active scope, we just wrote, now, add this:

 named_scope :contains, lambda { |column, text|   {:conditions => ["lower(#{column}) LIKE ?", "%#{text.downcase}%"]} }

The LIKE version of the query can now be written as:

user.active.contains(:last_name, "rappin")

And if I want the results to be sorted, I just add another scope:

user.active.contains(:last_name, "rappin").by_name

I find that to be readable and much clearer in describing the intent of the code.

Another nice feature of named scopes is that they maintain their find options in an attribute called proxy_options allowing for some amount of testing of the behavior of the scope:

should "correctly generate conditions for a contains scope" do
  expected = {:conditions => [lower(last_name) LIKE ?", "rappin"])
  assert_equal(expected, user.contains(:last_name, "rappin").proxy_options
end

One gotcha in that testing process is that each scope only contains its own options in that attribute, not the merged set of all composed options. Each option maintains an attribute which points to the earlier scope, called proxy_scope, so you can test the outer part of the composed scope as follows:

should "correctly generate a composed scope" do
  actual = user.is_active.contains(:last_name, "rappin")
  expected = {:conditions => {:is_active => true}}
  assert_equal(expected, actual.proxy_scope.proxy_options
end

Another thing to keep in mind for composed named scopes is that internally, they are implemented as nested calls to the existing ActiveRecord with_scope method. This means that the with_scope rules for composing options apply. Any :include options are merged together and duplicates removed, any conditions option are merged together and connected with logical and operators, and for all other options (such as :order the last scope to set the option wins.

There's another neat trick to scopes -- you can create anonymous ones on the fly. Check out this Ryan Bates screencast on the topic, then come back here in a few days when I take that mechanism another step or two farther.

Topics:

Comments: 12 so far

  1. Gotta love ‘em!

    I hope for round two, they get rid of the ‘lamba’ nonsense and just accept a &block. Much more rails-ish.

    Comment by Karl, Friday, June 20, 2008 @ 5:57 pm

  2. Nice article. I’ve been putting off upgrading my 1.x application, but you’ve just given me more incentive to do so.

    Comment by Harry Bailey, Friday, June 20, 2008 @ 6:02 pm

  3. [...] Pathfinder Development ยป Named Scopes Are Awesome Named Scopes Are Awesome (tags: activerecord rails ruby) SHARETHIS.addEntry({ title: “links for 2008-06-21″, url: “http://blog.libinpan.com/2008/06/21/links-for-2008-06-21/” }); [...]

    Pingback by links for 2008-06-21 | Libin Pan, Saturday, June 21, 2008 @ 1:44 am

  4. Nice post - I’m loving named_scope aswell - realised the other day that it’s easy to use them with has_many associations.

    Comment by Darragh Curran, Saturday, June 21, 2008 @ 7:05 am

  5. Don’t forget how well this blends into things like pagination:


    @users = User.active.by_name.paginate(
    :page => params[:page],
    :order => ‘created_on DESC’,
    :per_page => (params[:users_per_page] || 12)
    )

    For example :)

    I also like how I could pull an array as well (without making a new named_scope or tinkering with sorts):


    @users = User.active.by_name[0..3]

    I’m glad this made it into production.

    Comment by Michael Christenson II, Thursday, June 26, 2008 @ 9:58 am

  6. Um, just a distinction… In all your examples don’t you mean to use User instead of user? Your calling the method on the Model, not an instance? Correct me if I’m wrong.

    Comment by Britt, Thursday, June 26, 2008 @ 12:50 pm

  7. [...] other words, you can augment the contains scope introduced in last week’s post with a regular find method like [...]

    Pingback by Pathfinder Development » More Named Scope Awesomeness, Friday, June 27, 2008 @ 2:19 pm

  8. Am I missing something or is: “lower(#{column}) LIKE ?”, “%#{text.downcase}%” opening up the query to sql injection if you pass in something from the user, like: Model.contains(:name, params[:name]). I believe we’d want this instead:
    “lower(#{column}) LIKE ?”, “%#{connection.quote_string(text.downcase)}%”

    Comment by Drew Blas, Friday, June 27, 2008 @ 7:22 pm

  9. [...] Named scopes in Rails 2.1 : imagine you can get all your recent active users by user.recent.active ? Take a look to this awesome feature. [...]

    Pingback by DotMana » » News from RoR world, Thursday, July 10, 2008 @ 8:19 am

  10. [...] Pathfinder Development Round Two… [...]

    Pingback by links for 2008-07-10 | iLenceel, Thursday, July 10, 2008 @ 5:49 pm

  11. @Karl lambda allows the lazy evaluation of that code, a block would be ran at loading time..

    Comment by Luca Guidi, Tuesday, August 5, 2008 @ 7:03 am

  12. [...] Great resource on named scopes [...]

    Pingback by Scott Motte » Blog Archive » Named Scopes, Wednesday, August 27, 2008 @ 6:00 pm

Leave a comment

Powered by WP Hashcash

About Pathfinder

  • We design and build extraordinary applications for companies looking to make the next great idea a reality.
  • learn more

Topics

WordPress

Comments about this site: info@pathf.com