Named scopes in Rails are great, everybody knows that. They’re usually used to create granular, chainable sets of SQL conditions that nicely encapsulate your domain query logic. Here’s a simple example:
1 2 3 4 5 6 7 8 9 10 11 12 |
class Article < ActiveRecord::Base # Get all articles that have been published named_scope :published, :conditions => ['published = ?', true] # Get all articles that were created recently named_scope :recent, lambda { { :conditions => ['created_at >= ?', 1.week.ago] } } end # Get all recently created articles that have been published Article.published.recent #=> [<Article id: ...>, <..>] |
However, as much as I use named_scope for this purpose, I also use it for some smaller and still useful functions. For instance, I find that I often need to just fetch the first X number of results for any particular query. Instead of having to call find with the :limit option you could create the following named_scope:
1 2 3 4 5 6 7 8 9 |
class Article < ActiveRecord::Base # Only get the first X results named_scope :limited, lambda { |num| { :limit => num } } end # Get the first 5 articles - instead of Article.find(:all, :limit => 5) Article.limited(5) #=> [<Article id: ...>, <..>] |
Hey, any less typing I’ll take, and I find myself using this limited named_scope a lot. But let’s pimp it a little so that you don’t always have to supply the number, and make it default to the per_page value that exists on the class if you’re using will_paginate.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
class Article < ActiveRecord::Base # Only get the first X results. If no arg is given then try to # use the per_page value that will_paginate uses. If that # doesn't exist then use 10 named_scope :limited, lambda { |*num| { :limit => num.flatten.first || (defined?(per_page) ? per_page : 10) } } def per_page; 15; end end # Get the first 15 articles Article.limited #=> [<Article id: ...>, <..>] # Get the first 5 articles Article.limited(5) #=> [<Article id: ...>, <..>] |
Note that we have to use the variable length *num argument in the lambda to allow for no arguments.
Cool, so we’ve got a handy little tool for our toolbox now. Here’s another one I find myself using that isn’t strictly a conditional scope – ordered:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
class Article < ActiveRecord::Base # Order the results by the given argument, or 'created_at DESC' # if no arg is given named_scope :ordered, lambda { |*order| { :order => order.flatten.first || 'created_at DESC' } } end # Get all articles ordered by 'created_at DESC' Article.ordered #=> [<Article id: ...>, <..>] # Get all articles ordered by 'updated_at DESC' Article.ordered('updated_at DESC') #=> [<Article id: ...>, <..>] |
Be careful with this one, however, as with_scope (which is really what is powering named_scope) doesn’t know how to handle multiple order clauses. So, you can only used ordered once per call chain.
I’ve bundled these scopes up into a “utility scopes:http://github.com/yfactorial/utility_scopes” plugin/gem if you think they look useful to you. I’ve also added some class-level convenience initializers to let you override the default values (like the default limit and default order clause):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
class Article < ActiveRecord::Base # This class's default ordering (if not specified # defaults to 'created_at DESC' ordered_by 'published_at DESC' # By default, return 15 results (if not specified # defaults to 10 default_limit 15 end # Get the first 15 articles ordered by 'published_at DESC' Article.ordered.limited #=> [<Article id: ...>, <..>] # Get the first 15 articles ordered by 'popularity ASC' Article.ordered('popularity ASC').limited #=> [<Article id: ...>, <..>] # Get the first 20 articles ordered by 'popularity ASC' Article.ordered('popularity ASC').limited(20) #=> [<Article id: ...>, <..>] |
Need a little something else? How about this with scope I’ve included which will eager load the specified associations:
1 2 3 4 5 6 7 8 9 |
class Article < ActiveRecord::Base has_many :comments has_many :contributors, :class_name => 'User' end # Get the first 10 articles along with their comments, comment authors and article contributors # This is equivalent to # Article.limit(10).find(:all, :include => [{ :comments => :author }, :contributors]) Article.limit(10).with({ :comments => :author }, :contributors) |
You can get all these goodies yourself by doing the following in your Rails 2.1 app. In config/environment.rb specify the gem dependency:
1 2 3 4 5 |
Rails::Initializer.run do |config| # ... config.gem "yfactorial-utility_scopes", :lib => 'utility_scopes', :source => 'http://gems.github.com/' end |
And then to get the utility_scopes gem actually installed on your system:
rake gems:install GEM=yfactorial-utility_scopes |
Or you can just install the gem as you normally would:
sudo gem install yfactorial-utility_scopes -s http://gems.github.com |
Independent of whether or not you find these scopes useful, remember that named_scope is all up in your queries’ bidness – not just your queries’ conditions
Have some utility scopes you find to be indispensable? Let me know here or send me a request on github (user is yfactorial).
tags: ruby, rubyonrails

Excellent write-up. Very insightful uses of named_scope.
Careful with :order, because named_scope is really a cute way of wrapping up calls to with_scope, which doesn’t officially support :order (“parameters may include the :conditions, :joins, :include, :offset, :limit, and :readonly options”). It superficially works if you’re only using one scope, as in your examples, but with_scope doesn’t know how to combine multiple :orders so you’ll run into problems if you try to compose multiple ordered scopes.
In my experience it’s just been easier to toe the line and only use options that with_scope supports properly, and leave the ordering as a parameter to #find.
Article.limit(10).with({ :comments => :author }, :contributors)
What is #with ? Where can I find more information about that?
Tom: You’re right about
:ordernot being a supportedwith_scopeoption. This is something I run into every once and awhile but still doesn’t take away from the usefulness of having a limit operator. Your point is 100% correct however, and I should make a note of such.Christopher:
withis another utility scope provided by the plugin. Basically, all it does is provide a scope where the parameters towithare used as the value of the:includeoption in your normalfindcall. So your above example is equivalent to:Thanks for this wonderful article – I always wanted to try named_scopes – now I got new ideas how to use it !
It is also worth mentioning that named scopes sometimes behave a little wicked when used with AR calculations. This holds true when using options like :select and :group which are – at the moment – not considered in AR calculations.
Here’s a little snippet that I uploaded yesterday – beware, it’s not refactored.
http://pastie.org/pastes/255900
I use will_paginate which itself uses the count method on the collection. I’m doing joins to I need to group the result by id in order to avoid duplicate records. However, if I don’t use compact (see last line), I get back a scope with lots of nil records. Or, to put it more simply: scope.count doesn’t equal scope.all.size.
I’m already working on a fix, btw.
You can avoid using splatted arguments to your named_scope lambda by using Proc.new instead, whose arguments are already optional.
Another interesting use of named scopes is in creating composite versions of models with calculated attributes.
For example:
class Article < ActiveRecord::Base named_scope :with_total_articles_published, :select => ‘articles., COUNT() AS total_articles_published’, :group => ‘user_id’ end
Article.with_total_articles_published.each do |aggregate_article| puts “User #{aggregate_article.user_id} has published” \ ” #{pluralize(aggregate_article.total_articles_published, ‘article’)}” end
I like it.
Also, can you release to rubyforge so that the gem is more easily used as a dependency by other gems, and to make the install process “normal” for gem users? Merci.
As a bonus, it probably makes all the instructions above simpler.
config.gem "yfactorial-utility_scopes", :lib => 'utility_scopes', :source => 'http://gems.github.com/'becomes
Dr. Nic – your request has been denied due to the incompetence of the maintainer (me) in publishing the gem to rubyforge with hoe. May give it a go later on, may not.
Ryan a plesure as always scanning your tips. Many thanks for an invaluable blog