Simplify ActiveRecord Aggregates and Other Goodies via named_scope
We’ve been huge fans of named_scope since its introduction in Rails 2.1. If you are new to the named_scope concept, please read Ryan Daigle’s intro to named_scope and watch Ryan Bates’ named_scope RailsCast. Ryan Daigle also had a good follow up article explaining his utility scope gem.
With a recent project, we really started utilizing extensions with named_scope which is very powerful and cleaned up our code considerably.
For example, let’s say you have an issue tracking system that has an Entry model that belongs_to both a User model and a Project model. Using named_scope, we can easily access Entries for a date range filtered to both the Project and User level using named_scope lambdas:
<code class='ruby'>
class Entry < ActiveRecord::Base
named_scope :by_date, lambda { |*args|
{
:order => "starts_at asc",
:conditions => ["starts_at BETWEEN ? AND ?", args[0].beginning_of_day.to_s(:db), (args[1]||Time.now).end_of_day.to_s(:db)]
} }
end
To find the Entries for the last week for the first user, we would simply do:
<code class='ruby'>u = User.first u.entries.by_date(1.week.ago, Time.now)
To find the Entries for the last week for the first project, we would simply do:
<code class='ruby'>p = Project.first p.entries.by_date(1.week.ago, Time.now)
What happens if we want to see the sum of hours worked for a date range? This is where extensions come in very handy. Let’s add a “total_duration” extension to our named_scope:
<code class='ruby'>
class Entry < ActiveRecord::Base
named_scope :by_date, lambda { |*args|
{
:order => "starts_at asc",
:conditions => ["starts_at BETWEEN ? AND ?", args[0].beginning_of_day.to_s(:db), (args[1]||Time.now).end_of_day.to_s(:db)]
} } do
def total_duration
self.sum(:duration)
end
end
end
Now to get the duration for the past week for the first user we simply do:
<code class='ruby'>u = User.first u.entries.by_date(1.week.ago, Time.now).total_duration
with similar logic at the project level.
To group all of the entries by project, we would do:
<code class='ruby'>
class Entry < ActiveRecord::Base
named_scope :by_date, lambda { |*args|
{
:order => "starts_at asc",
:conditions => ["starts_at BETWEEN ? AND ?", args[0].beginning_of_day.to_s(:db), (args[1]||Time.now).end_of_day.to_s(:db)]
} } do
def duration_by_project
self.sum(:duration, :group => :project)
end
end
end
If you need the lambda arguments inside your named_scope extension, you will need to use “self.proxy_options”. For example, let’s say you need the from and to dates in order to calculate the average duration per weekday. That code would look something like this:
<code class='ruby'>
class Entry < ActiveRecord::Base
named_scope :by_date, lambda { |*args|
{
:order => "starts_at asc",
:conditions => ["starts_at BETWEEN ? AND ?", args[0].beginning_of_day.to_s(:db), (args[1]||Time.now).end_of_day.to_s(:db)]
} } do
def duration_by_project_per_weekday
from_date = DateTime.parse(self.proxy_options[:conditions][1])
to_date = DateTime.parse(self.proxy_options[:conditions][2])
weekday_count = weekday_count(from_date, to_date)
totals = self.sum(:duration, :group => :project)
totals.map {|a| [a[0].display_name, ( a[1].to_f / weekday_count)]}
end
end
end
In the above example, “weekday_count” is a custom method we created to calculate the number of weekdays between 2 given dates.
The opportunities to simplify code using named_scope is great. As I was writing this post, one of our team members, Jason Derrett, created a named_scope to wrap a Xapian full text search into a Lesson model:
<code class='ruby'>
class Lesson < ActiveRecord::Base
named_scope :find_with_xapian, lambda { |*args| {:conditions => ["lessons.id in (?)", ActsAsXapian::Search.new([Lesson], args.to_s).results.collect{|x| x[:model].id}]}}
end
In this example, using Xapian to search for a keyword becomes as simple as:
<code class='ruby'>Lesson.find_with_xapian "fishing"
How cool is that?!?
Are there any other cool named_scope tricks out there that any of you are using?

Older articles
Latest comments
Archives
Tweetstream