Rails 3: How to Dynamically Build Complex Queries with Composed Scopes

In previous posts, I wrote how to build complex search queries in Rails 2 using the ez_where plugin. In Rails 3, such plugins and long-winded code is completely unnecessary thanks to the powerful ActiveRelation query interface.

In a project I’m working on, I needed to be able to build just such a complex search so that users could find a customer whose name or email matched one or several search terms. The search terms would be supplied as a space-separated string. For example, to find all the customers whose name (first_name or last_name) or email contained ‘joe bloggs’, the SQL fragment would look something like:

SELECT * FROM customers WHERE (customers.first_name ILIKE '%term_1%' OR customers.last_name ILIKE '%term_1%' OR customers.email ILIKE '%term_1%') AND (customers.first_name ILIKE '%term_2%' OR customers.last_name ILIKE '%term_2%' OR customers.email ILIKE '%term_2%');

Achieving this in ActiveRelation is deceptively simple, but most tutorials I’ve read only discuss how to build simple named scopes that can be chained together. I needed to be able to dynamically build the scope based on any number of search terms the user might enter.

Reading the API docs, I discovered ActiveRecord::Base.scoped which returns an empty scope object upon which you can build in your code. Here’s an example that generates the SQL fragment above:

class Customer < ActiveRecord::Base
  scope :terms, lambda { |terms|
    return if terms.blank?
 
    composed_scope = self.scoped
 
    terms.split(' ').each do |term|
      term = '%' << term << '%' # Wrap the search term in % to achieve 'fuzzy' searching
      composed_scope = composed_scope.where('first_name ILIKE ? OR last_name ILIKE ?' OR email ILIKE ?', term, term, term)
    end
 
    composed_scope
  }
end

With this scope, you can perform complex searches that span multiple attributes (in this case first_name, last_name, and email) with ease. What’s more, because the returned composed_scope is an ActiveRelation, you can continue the chain in your code, for example:

Customer.terms('joe bloggs').order('first_name').where('active = ?', true).all

Rails: Multiple default scopes for ActiveRecord

Default scopes were introduced in Rails 2.3 to allow a default set of options to be applied to any find methods. The common example is to always order a set of results by a given column, e.g:

class Post < ActiveRecord::Base
  # Any calls to Post.find will automatically have the default :order option merged into them
  # Post.find(:all)
  # => SELECT * FROM "posts" ORDER BY "created_at DESC";
  default_scope :order => "created_at DESC"
end

Unlike named_scopes (which I am finding more and more useful every day), I found that default scopes cannot be combined when I tried to use the acts_as_revisable and is_paranoid plugins together:

class Post < ActiveRecord::Base
  acts_as_revisable
  is_paranoid
end

It seems the default scope declared in is_paranoid overrides that of acts_as_revisable. Post.find(:all) will therefore return every revision of Post rather than just the current revision. You can check this out by reversing the plugin order:

class Post < ActiveRecord::Base
  is_paranoid
  acts_as_revisable
end

Now, Post.find(:all) will return only current revisions, but will include any destroyed posts as the acts_as_revisable default scope overrides is_paranoid!

A Solution

This post and code snippet shows a method for declaring multiple default scopes on a model. I’ve not yet tried out the code, though, as one of the commenter’s was kind enough to forkis_paranoid and modify it to merge any existing default scopes. With this forked plugin, Post can be scoped correctly by both plugins.

The fork is available at http://github.com/grioja/is_paranoid/tree/master.

But… is_paranoid is depracated

I noticed that the original is_paranoid plugin has ceased development, so I’m not sure if I’ll continue to use it, although It’s a neat little plugin, and has several forks.

The underlying problem, though, of ActiveRecord allowing only one default_scope to be declared, is something that I’m bound to come up against in the future, so it’s handy to know there is a workaround at least until Rails includes the functionality.

As an aside, Rich Cavanaugh, the developer of acts_as_revisable, has pointed out that the plugin includes some basic is_paranoid functionality already (see Rich’s reply to my original ramblings):

class Post < ActiveRecord::Base
  acts_as_revisable :on_delete => :revise
end

Do you use default_scope? Do you find the single scope a limitation, or do you rely on named scopes? Feel free to discuss in the comments.

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!

Rails: Validating Unique Attributes with acts_as_revisable

I’ve been using the acts_as_revisable plugin in my Rails apps to store a revision history of ActiveRecord models. The plugin automatically versions your models, and allows you to navigate revisions; branch and merge changes, and perform bulk changesets.

During some tests of my code, I found a conflict between the plugin and ActiveRecord.validates_uniqueness_of. It seems the conflict occurs because acts_as_revisable stores all revisions in the same database table. For example, when trying to save a record whose :name attribute should be unique, the validator will see all previous versions of the record with identical names, and prevent the save.

The fix I put in for this was to use the :scope to scope validates_uniqueness_of by the revisable_is_current attribute:

class User < ActiveRecord::Base
  validates_as_unique :username, :scope => :revisable_is_current
end

Scoping the attribute causes the validator to only consider records whose :revisable_is_current attribute matches that of the record being edited. This works for my app, as only the current revision (User#revisable_is_current= true) will ever be edited.

Are you using acts_as_revisable and come across this problem? Spotted a better solution? Feel free to discuss ideas in the comments.

Rails: Abstract Models – ActiveRecord Without Tables

I recently hit a situation where I needed an ActiveRecord model with no corresponding database table. The model had to represent a collection of other event-type models – for example: Birthdays, Weddings, Anniversaries, etc.

As an Event model would be a completely abstract representation of another model, it didn’t make sense to store them in the database. Unfortunately, ActiveRecord doesn’t appear to support this natively. Luckily, I came across this post by Micha? Szajbe which demonstrates ActiveRecord model without a corresponding table. I’ve used Micha?’s code to build the example model below:

Event-Model Example

First, create a class to represent your abstract model using the code from the post above:

# app/models/event.rb
class Event < ActiveRecord::Base
  def self.columns
    @columns ||= []
  end
 
  def self.column(name, sql_type = nil, default = nil, null = true)
    columns << ActiveRecord::ConnectionAdapters::Column.new(name.to_s, default, sql_type.to_s, null)
  end
 
  # Manually define the columns used by this model
  column :name, :string
  column :event_at, :datetime
  column :description, :string
  column :event_type, :string
end

(Note that I’ve since found that ActiveRecord doesn’t like any column called id)

In my example, I gave each of the real models a to_event method to capture the common attributes and return an Event model:

# app/models/birthday.rb
def to_event
  Event.new(
    :name => [self.person.name, "'s birthday"].join,
    :event_at self.birthday,
    :event_type => 'birthday'
  )
end

Now we can build a normal RESTful controller with index and show actions to display the Events. Note that because the models are just an abstract view on other models, the CRUD RESTful routes (create, edit, etc.) are redundant.

script/generate rspec_controller events index

In the controller, build an array of events by calling to_event on your other models:

# app/controllers/events_controller.rb
def index
  @events = Wedding.find(:all).collect { |w| w.to_event }
  @events += Birthdays.find(:all).collect { |b| b.to_event }
end

Finally, add a RESTful route for your events into routes.rb:

# config/routes.rb
map.resources :events

Now, the abstract event models can be accessed using the normal RESTful URL:

http://localhost/events

References

Tableless Models in Rails [Micha? Szajbe, http://codetunes.com/]