4.14. NamedScopes

When programming Rails applications, it is sometimes clearer and simpler to define frequent searches as separate methods. In Rails speak, these are referred to as NamedScope. These NamedScopes can be chained, just like other methods.

Preparation

We are building our own little online shop:
$ rails new shop
  [...]
$ cd shop
$ rails generate model product name 'price:decimal{7,2}' weight:integer in_stock:boolean expiration_date:date
  [...]
$ rake db:migrate
  [...]
$
Please populate the file db/seeds.rb with the following content:
# ruby encoding: utf-8

Product.create(name: 'Milk (1 liter)', weight: 1000, in_stock: true, price: 0.45, expiration_date: Date.today + 14.days)
Product.create(name: 'Butter (250 g)', weight: 250, in_stock: true, price: 0.75, expiration_date: Date.today + 14.days)
Product.create(name: 'Flour (1 kg)', weight: 1000, in_stock: false, price: 0.45, expiration_date: Date.today + 100.days)
Product.create(name: 'Jelly Babies (6 x 300 g)', weight: 1500, in_stock: true, price: 4.96, expiration_date: Date.today + 1.year)
Product.create(name: 'Super-Duper Cake Mix', in_stock: true, price: 11.12, expiration_date: Date.today + 1.year)
Product.create(name: 'Eggs (12)', in_stock: true, price: 2, expiration_date: Date.today + 7.days)
Product.create(name: 'Peanuts (8 x 200 g bag)', in_stock: false, weight: 1600, price: 17.49, expiration_date: Date.today + 1.year)
Now drop the database and repopulate it with the db/seeds.rb:
$ rake db:reset
  [...]
$ 

Simple NamedScopes

If we want to count products that are in stock in our online shop, then we can use the following query each time:
$ rails console
Loading development environment (Rails 3.2.9)
>> Product.where(in_stock: true).count
   (0.2ms)  SELECT COUNT(*) FROM "products" WHERE "products"."in_stock" = 't'
=> 5
>> exit
$
But we could also define a NamedScope available in the app/models/product.rb:
class Product < ActiveRecord::Base
  attr_accessible :in_stock, :name, :price, :weight

  scope :available, where(in_stock: true)
end
And then use it:
$ rails console
Loading development environment (Rails 3.2.9)
>> Product.available.count
   (0.1ms)  SELECT COUNT(*) FROM "products" WHERE "products"."in_stock" = 't'
=> 5
>> exit
$
Let's define a second NamedScope for this example in the app/models/product.rb:
class Product < ActiveRecord::Base
  attr_accessible :in_stock, :name, :price, :weight

  scope :available, where(:in_stock => true)
  scope :cheap, where(price: 0..1)
end
Now we can chain both named scopes to output all cheap products that are in stock:
$ rails console
Loading development environment (Rails 3.2.9)
>> Product.cheap.count
   (0.1ms)  SELECT COUNT(*) FROM "products" WHERE ("products"."price" BETWEEN 0 AND 1)
=> 3
>> Product.cheap.available.count
   (0.2ms)  SELECT COUNT(*) FROM "products" WHERE "products"."in_stock" = 't' AND ("products"."price" BETWEEN 0 AND 1)
=> 2
>> exit
$

Lambda

If we want to set up a named scope consumable that compares today's date with the value of expiration_date, then we have to use lambda for this. A normal named scope is defined at the start time of your Rails application by ActiveRecord and then reused over and over again. This means, a Date.today (for today's date) would only be converted into a date once than then reused. So tomorrow and next week it would still be today's date. If we define the NamedScope with lambda, this lambda is reconstructed with each new call.
app/models/product.rb
class Product < ActiveRecord::Base
  attr_accessible :expiration_date, :in_stock, :name, :price, :weight

  scope :consumable, lambda { where('expiration_date > ?', Date.today) }
end
This gives us correctly the food that is not yet past its expiration date today:
$ rails console
Loading development environment (Rails 3.2.9)
>> Product.consumable.count
   (0.1ms)  SELECT COUNT(*) FROM "products" WHERE (expiration_date > '2012-11-18')
=> 7
>> exit
$

Passing Parameters

If you need a NamedScope that can also process parameters, then that is no problem either. The following example outputs products that are cheaper than the specified value. The app/models/product.rb looks like this:
class Product < ActiveRecord::Base
  attr_accessible :in_stock, :name, :price, :weight

  scope :cheaper_than, lambda { |price| where('price < ?', price) }
end
Now we can count all products that cost less than 50 cent:
$ rails console
Loading development environment (Rails 3.2.9)
>> Product.cheaper_than(0.5).count
   (0.1ms)  SELECT COUNT(*) FROM "products" WHERE (price < 0.5)
=> 2
>> exit
$

Creating New Records with NamedScopes

Let's use the following app/models/product.rb:
class Product < ActiveRecord::Base
  attr_accessible :in_stock, :name, :price, :weight

  scope :available, where(:in_stock => true)
end
With this NamedScope we can not only find all products that are in stock, but also create new products that contain the value true in the field in_stock:
$ rails console
Loading development environment (Rails 3.2.9)
>> product = Product.available.build
=> #<Product id: nil, name: nil, price: nil, weight: nil, in_stock: true, expiration_date: nil, created_at: nil, updated_at: nil>
>> product.in_stock
=> true
>> exit
$ 
This works with the method build (see the section called “build”) and create (see the section called “create”).