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
