Free Trial

Safari Books Online is a digital library providing on-demand subscription access to thousands of learning resources.


  • Create BookmarkCreate Bookmark
  • Create Note or TagCreate Note or Tag
  • DownloadDownload
  • PrintPrint

9.1. Scopes

Scopes (or “named scopes” if you’re old school) allow you define and chain query criteria in a declarative and reusable manner.

class Timesheet < ActiveRecord::Base
  scope :submitted, where(:submitted => true)
  scope :underutilized, where('total_hours < 40')

To declare a scope, use the scope class method, passing it a name as a symbol and some sort of query definition. If your query is known at load time, you can simply use Arel criteria methods like where, order, and limit to construct the definition as shown in the example. On the other hand, if you won’t have all the parameters for your query until runtime, use a lambda as the second parameter. It will get evaluated whenever the scope is invoked.

class User < ActiveRecord::Base
  scope :delinquent, lambda { where('timesheets_updated_at < ?',
1.week.ago)}

Invoke scopes as you would class methods.

>> User.delinquent
=> [#<User id: 2, timesheets_updated_at: "2010-01-07 01:56:29"...>]

9.1.1. Scope Parameters

You can pass arguments to scope invocations by adding parameters to the lambda you use to define the scope query.

class BillableWeek < ActiveRecord::Base
  scope :newer_than, lambda { |date| where('start_date > ?', date) }

Then pass the argument to the scope as you would normally.

BillableWeek.newer_than(Date.today)

9.1.2. Chaining Scopes

One of the beauties of scopes is that you can chain them together to create complex queries from simple ones:

>> Timesheet.underutilized.submitted
=> [#<Timesheet id: 3, submitted: true, total_hours: 37 ...

Scopes can be chained together for reuse within scope definitions themselves. For instance, let’s say that we always want to constrain the result set of underutilized to submitted timesheets:

class Timesheet < ActiveRecord::Base
  scope :submitted, where(:submitted => true)
  scope :underutilized, submitted.where('total_hours < 40')

9.1.3. Scopes and has_many

In addition to being available at the class context, scopes are available automatically on has_many association attributes.

>> u = User.find 2
=> #<User id: 2, login: "obie"...>

>> u.timesheets.size
=> 3
>> u.timesheets.underutilized.size
 => 1

9.1.4. Scopes and Joins

You can use Arel’s join method to create cross-model scopes. For instance, if we gave our recurring example Timesheet a submitted_at date attribute instead of just a boolean, we could add a scope to User allowing us to see who is late on their timesheet submission.

scope :tardy, lambda {
  joins(:timesheets).
  where("timesheets.submitted_at <= ?", 7.days.ago).
  group("users.id")
}

Arel’s to_sql method is useful for debugging scope definitions and usage.

>> User.tardy.to_sql
=> "SELECT users.* FROM users
    INNER JOIN timesheets ON timesheets.user_id = users.id
    WHERE (timesheets.submitted_at <= '2010-07-06 15:27:05.117700')
    GROUP BY users.id" # query formatted nicely for the book

Note that as demonstrated in the example, it’s a good idea to use unambiguous column references (including table name) in cross-model scope definitions so that Arel doesn’t get confused.

9.1.5. Scope Combinations

Our example of a cross-model scope violates good object-oriented design principles: it contains the logic for determining whether or not a Timesheet is submitted, which is code that properly belongs in the Timesheet class. Luckily we can use Arel’s merge method (aliased as &) to fix it. First we put the late logic where it belongs, in Timesheet:

scope :late, lambda { where("timesheet.submitted_at <= ?", 7.days.ago) }

Then we use our new late scope in tardy:

scope :tardy, lambda {
  joins(:timesheets).group("users.id") & Timesheet.late
}

If you have trouble with this technique, make absolutely sure that your scopes’ clauses refer to fully qualified column names. (In other words, don’t forget to prefix column names with tables.) The console and to_sql method is your friend for debugging.

9.1.6. Default Scopes

There may arise use cases where you want certain conditions applied to the finders for your model. Consider our timesheet application has a default view of open timesheets—we can use a default scope to simplify our general queries.

class Timesheet < ActiveRecord::Base
  default_scope :where(:status => "open")
end

Now when we query for our Timesheets, by default the open condition will be applied:

>> Timesheet.all.map(&:status)
=> ["open", "open", "open"]

Default scopes also get applied to your models when building or creating them, which can be a great convenience or a nuisance if you are not careful. In our previous example, all new Timesheets will be created with a status of “open.”

>> Timesheet.new
=> #<Timesheet id: nil, status: "open">
>> Timesheet.create
=> #<Timesheet id: 1, status: "open">

You can override this behavior by providing your own conditions or scope to override the default setting of the attributes.

>> Timesheet.where(:status => "new").new
=> #<Timesheet id: nil, status: "new">
>> Timesheet.where(:status => "new").create
=> #<Timesheet id: 1, status: "new">

There may be cases where at runtime you want to create a scope and pass it around as a first class object leveraging your default scope. In this case, Active Record provides the scoped method.

>> timesheets = Timesheet.scoped.order("submitted_at DESC")
=> [#<Timesheet id: 1, status: "open"]
>> timesheets.where(:name => "Durran Jordan")
=> []

There’s another approach to scopes that provides a sleeker syntax, scoping, which allows the chaining of scopes via nesting within a block.

>> Timesheet.order("submitted_at DESC").scoping do
>>   Timesheets.all
>> end
=> #<Timesheet id: 1, status: "open">

That’s pretty nice, but what if we don’t want our default scope to be included in our queries? In this case Active Record takes care of us through the unscoped method.

>> Timesheet.unscoped.order("submitted_at DESC")
=> [#<Timesheet id: 2, status: "submitted">]

Similarly to overriding our default scope with a relation when creating new objects, we can supply unscoped as well to remove the default attributes.

>> Timesheet.unscoped.new
=> #<Timesheet id: nil, status: nil>

9.1.7. Using Scopes for CRUD

You have a wide range of Active Record’s CRUD methods available on scopes, which gives you some powerful abilities. For instance, let’s give all our underutilized timesheets some extra hours.

>> u.timesheets.underutilized.collect(&:total_hours)
=> [37, 38]

>> u.timesheets.underutilized.update_all("total_hours = total_hours + 2")
=> 2

>> u.timesheets.underutilized.collect(&:total_hours)
=> [37, 38] # whoops, cached result

>> u.timesheets(true).underutilized.collect(&:total_hours)
=> [39] # results after telling association to reload

Scopes including a where clause using hashed conditions will populate attributes of objects built off of them with those attributes as default values. Admittedly it’s a bit difficult to think of a plausible use case for this feature, but we’ll show it in an example. First, we add the following scope to Timesheet:

scope :perfect, submitted.where(:total_hours => 40)

Now, building an object on the perfect scope should give us a submitted timesheet with 40 hours.

> Timesheet.perfect.build
 => #<Timesheet id: nil, submitted: true, user_id: nil, total_hours: 40
...>

As you’ve probably realized by now, the new Arel underpinnings of Active Record are tremendously powerful and truly elevate the Rails 3 platform.