4.14. Scopes

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:
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
  [...]
$ 

Defining a Scope

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 4.0.0)
>> Product.where(in_stock: true).count
   (0.1ms)  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
  scope :available, -> { where(in_stock: true) }
end
And then use it:
$ rails console
Loading development environment (Rails 4.0.0)
>> Product.available.count
   (0.2ms)  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
  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 4.0.0)
>> 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
$

Passing in Arguments

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

Creating New Records with Scopes

Let's use the following app/models/product.rb:
class Product < ActiveRecord::Base
  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 4.0.0)
>> 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”).