in Uncategorized

Rails: Complex Queries with Named Scopes

I’ve recently come to realise the massive power of ActiveRecord’s named_scopes. The revelation came whilst I was researching how to scope, by their permissions, the records a @current_user can view or edit.

I’d looked at all sorts of plugins and complex authorization configurations, before realising that named scopes have almost limitless potential for building such complex queries.

What are named scopes?

As demonstrated by Ryan Bates’ screencast, named scopes let you declare finder conditions as methods on your model object.

For example, you could get all active users using

# app/models/user.rb
class User < ActiveRecord::Base
  named_scope :active, :conditions => { :active => true }
end
 
# To select all active users
User.active

The real power of named scopes, though, lies in their ability to be chained. As they are just methods on your model class, they can be combined, and ActiveRecord will take care of building the relevant conditions and joins:

# app/models/user.rb
class User < ActiveRecord::Base
  named_scope :active => :conditions => { :active => true }
  named_scope :admin => :conditions => { :admin => true }
  named_scope :recently_logged_in,
              lambda { { :conditions => ['logged_in_at >= ?', 7.days.ago]  } }
end
 
# To get all active users who have recently logged in:
User.active.recently_logged_in
 
# To get all active admin users who have recently logged in
User.active.admin.recently_logged_in

Using Named Scopes to Build Complex Queries

With chaining, you can build some pretty complex queries with ease. However, it would be great if we were able to declare some conditions at run-time, rather than when the model class is loaded.

The lambda function lets you do just that, taking a set of arguments and inserting them into your query:

# app/models/user.rb
class User < ActiveRecord::Base
  named_scope :admin, :conditions => { :admin => true }
  named_scope :last_logged_in, lambda { |time_ago|
    time_ago ||= 7.days.ago
    { :conditions => ['logged_in_at >= ?', time_ago] }
  }
end
 
# By default, we'll get users who have logged in in the last 7 days:
User.last_logged_in
 
# To get users who have logged in during the past 4 weeks:
User.last_logged_in(4.weeks.ago)
 
# To get admin users who have logged in in the past 6 months
User.last_logged_in(6.months.ago).admin

Combining lambda with named_scope lets you build complex attribute queries with runtime arguments. As an example, I’ve reworked my the code from my previous posts to make use of named_scope:

# app/models/user.rb
class User < ActiveRecord::Base
  named_scope :name_like, lambda { |terms|
    # ensure terms is an array
    terms = [*terms]
 
    composed_conditions = EZ::Caboose::Condition.new :users
 
    terms.each do |term|
      term = ['%', term, '%'].join
 
      condition = EZ::Caboose::Condition.new :users
      condition.append ['first_name ILIKE ?', term], :or
      condition.append ['last_name ILIKE ?', term], :or
 
      composed_conditions << condition
    end
 
    { :conditions => composed_conditions.to_sql }
  }
end

With this small chunk of code (and a helping hand from the Caboose EZ Condition plugin), you can now search for Users by matching their first or last names with the supplied terms, e.g:

# To search for users with a first or last name like "joe"
User.name_like("joe")
 
# To search for users with a first or last name like "joe" and a first or last name like "bloggs":
User.name_like(["joe", "bloggs"])

As we saw above, the greatest advantage of using named_scopes for this type of functionality is their ability to chain. With this in mind, I was able to solve my problem of scoping results within the permissions of @current_user:

# app/models/user.rb
class User < ActiveRecord
  # ...
  named_scope :visible_to lambda { |user|
    { :conditions => ['department_id IN ?', user.managed_department_ids] }
  }
end
 
User.last_logged_in(4.weeks.ago).name_like(["joe", "bloggs"]).visible_to(@current_user).ordered_by(:name)

Named scopes are undoubtedly one of the most powerful tools available to ActiveRecord. I can’t help but think I’m still just scratching the surface of their potential, but already I’ve been able to refactor pages of code and build powerful, Rails-friendly custom queries.

Are you using named scopes in your code? Feel free to discuss in the comments!

Tweet about this on TwitterShare on Google+Share on FacebookShare on LinkedInEmail this to someone