blog comments 9 del.icio.us bookmarks 0 diggs 2 Google results 0

3.8
PostRank

Named Scope: It's Not Just for Conditions, Ya Know?

From Ryan's Scraps - Blog, 3 months ago, 4 views

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: ...>, <..>]

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 the with scope which will eager load the specified associations:

1
2
3
4
5
6
7
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
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

comments

No comments yet.

You must be logged in to add your own comment.