Neu: Das englische Ruby on Rails 4.0 Buch.

4.14. NamedScopes

Manchmal ist es beim Programmieren von Rails-Applikationen übersichtlicher und einfacher, häufig auftauchende Suchen als eigene Methoden zu definieren. Diese heißen in Rails-Sprache NamedScope. Diese NamedScopes können wie andere Methoden hintereinander verkettet werden.

Vorbereitung

Wir bauen uns einen kleinen Online-Shop:
MacBook:~ xyz$ rails new shop
[...]
MacBook:~ xyz$ cd shop
MacBook:shop xyz$ rails generate model product name 'price:decimal{7,2}' weight:integer in_stock:boolean expiration_date:date
[...]
MacBook:shop xyz$ rake db:migrate
[...]
MacBook:shop xyz$
Füllen Sie bitte die Datei db/seeds.rb mit folgendem Inhalt:
# ruby encoding: utf-8

Product.create(:name => 'Milch (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 => 'Mehl (1 kg)', :weight => 1000, :in_stock => false, :price => 0.45, :expiration_date => Date.today + 100.days)
Product.create(:name => 'Gummibärchen (6 x 300 g)', :weight => 1500, :in_stock => true, :price => 4.96, :expiration_date => Date.today + 1.year)
Product.create(:name => 'Super-Duper Backmischung', :in_stock => true, :price => 11.12, :expiration_date => Date.today + 1.year)
Product.create(:name => 'Eier (12 Stück)', :in_stock => true, :price => 2, :expiration_date => Date.today + 7.days)
Product.create(:name => 'Erdnüsse (8 x 200 g Packung)', :in_stock => false, :weight => 1600, :price => 17.49, :expiration_date => Date.today + 1.year)
Jetzt die Datenbank löschen und neu mit der db/seeds.rb befüllen:
MacBook:shop xyz$ rake db:reset
[...]
MacBook:shop xyz$

Einfache NamedScopes

Wenn wir in unserem Online-Shop nur Produkte, die auf Lager sind anzeigen wollen, so könnten wir jedes Mal die folgende Abfrage machen:
MacBook:shop xyz$ rails console
Loading development environment (Rails 3.2.3)
1.9.3p194 :001 > Product.where(:in_stock => true)
  Product Load (0.2ms)  SELECT "products".* FROM "products" WHERE "products"."in_stock" = 't'
 => [#<Product id: 1, name: "Milch (1 Liter)", price: #<BigDecimal:7fd17c8cbbd0,'0.45E0',9(45)>, weight: 1000, in_stock: true, expiration_date: "2012-05-22", created_at: "2012-05-08 12:43:15", updated_at: "2012-05-08 12:43:15">, #<Product id: 2, name: "Butter (250 g)", price: #<BigDecimal:7fd17c8ca870,'0.75E0',9(45)>, weight: 250, in_stock: true, expiration_date: "2012-05-22", created_at: "2012-05-08 12:43:15", updated_at: "2012-05-08 12:43:15">, #<Product id: 4, name: "Gummibärchen (6 x 300 g)", price: #<BigDecimal:7fd17c8d13f0,'0.496E1',18(45)>, weight: 1500, in_stock: true, expiration_date: "2013-05-08", created_at: "2012-05-08 12:43:15", updated_at: "2012-05-08 12:43:15">, #<Product id: 5, name: "Super-Duper Backmischung", price: #<BigDecimal:7fd17c8cfd98,'0.1112E2',18(45)>, weight: nil, in_stock: true, expiration_date: "2013-05-08", created_at: "2012-05-08 12:43:15", updated_at: "2012-05-08 12:43:15">, #<Product id: 6, name: "Eier (12 Stück)", price: #<BigDecimal:7fd17c8d52e8,'0.2E1',9(36)>, weight: nil, in_stock: true, expiration_date: "2012-05-15", created_at: "2012-05-08 12:43:15", updated_at: "2012-05-08 12:43:15">] 
1.9.3p194 :002 > exit
MacBook:shop xyz$
Wir könnten aber auch in der app/models/product.rb einen NamedScope available definieren:
class Product < ActiveRecord::Base
  attr_accessible :in_stock, :name, :price, :weight

  scope :available, where(:in_stock => true)
end
Und diesen dann benutzen:
MacBook:shop xyz$ rails console
Loading development environment (Rails 3.2.3)
1.9.3p194 :001 > Product.available.count
   (0.2ms)  SELECT COUNT(*) FROM "products" WHERE "products"."in_stock" = 't'
 => 5 
1.9.3p194 :002 > Product.where(:in_stock => true).count
   (0.2ms)  SELECT COUNT(*) FROM "products" WHERE "products"."in_stock" = 't'
 => 5 
1.9.3p194 :003 > exit
MacBook:shop xyz$
Lassen Sie uns für das Beispiel einen zweiten NamedScope in der app/models/product.rb definieren:
class Product < ActiveRecord::Base
  attr_accessible :in_stock, :name, :price, :weight

  scope :available, where(:in_stock => true)
  scope :cheap, where(:price => [0..1])
end
Jetzt können wir alle günstigen (cheap) Produkte ausgeben, die auf Lager sind:
MacBook:shop xyz$ rails console
Loading development environment (Rails 3.2.3)
1.9.3p194 :001 > Product.cheap.count
   (0.3ms)  SELECT COUNT(*) FROM "products" WHERE (("products"."price" BETWEEN 0 AND 1 OR "products"."price" IN (NULL)))
 => 3 
1.9.3p194 :002 > Product.cheap.available.count
   (0.4ms)  SELECT COUNT(*) FROM "products" WHERE "products"."in_stock" = 't' AND (("products"."price" BETWEEN 0 AND 1 OR "products"."price" IN (NULL)))
 => 2 
1.9.3p194 :003 > exit
MacBook:shop xyz$ 

Lambda

Wenn wir einen NamedScope consumable einbauen möchten, der das heutige Datum mit dem Wert von expiration_date vergleicht, dann müssen wir dafür mit Lambda arbeiten. Ein normaler NamedScope wird einmal von ActiveRecord definiert und dann immer wieder benutzt. Das heißt, ein Date.today (für das heutige Datum) würde nur einmal in ein Datum umgesetzt und danach immer wieder genommen. Morgen würde also immer noch mit dem Datum von heute gearbeitet. Wenn wir den NamedScope mit Lambda definieren, so wird dieses Lambda bei jedem Aufruf neu aufgebaut.
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
Damit erhalten wir immer korrekt die heute noch essbaren Lebensmittel:
MacBook:shop xyz$ rails console
Loading development environment (Rails 3.2.3)
1.9.3p194 :001 > Product.consumable.count
   (0.1ms)  SELECT COUNT(*) FROM "products" WHERE (expiration_date > '2012-05-08')
 => 7 
1.9.3p194 :002 > exit
MacBook:shop xyz$

Parameter übergeben

Wenn Sie einen NamedScope brauchen, der auch Parameter verarbeiten kann, so ist das auch kein Problem. Das folgende Beispiel gibt Produkte aus, die günstiger sind als der angegebene Wert. Die app/models/product.rb sieht wie folgt aus:
class Product < ActiveRecord::Base
  attr_accessible :in_stock, :name, :price, :weight

  scope :cheaper_than, lambda { |price| where('price < ?', price) }
end
Jetzt können wir alle Produkte zählen, die weniger als 50 cent kosten:
MacBook:shop xyz$ rails console
Loading development environment (Rails 3.2.3)
1.9.3p194 :001 > Product.cheaper_than(0.5).count
   (0.2ms)  SELECT COUNT(*) FROM "products" WHERE (price < 0.5)
 => 2 
1.9.3p194 :002 > exit
MacBook:shop xyz

Neue Datensätze mit NamedScopes anlegen

Nehmen wir folgende 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])
  scope :cheaper_than, lambda { |price| where('price < ?', price) }
end
Mit diesem NamedScope können wir dann nicht nur alle Produkte heraussuchen, die auf Lager sind, sondern auch neue Produkte anlegen, die im Feld in_stock den Wert true enthalten:
MacBook:shop xyz$ rails console
Loading development environment (Rails 3.2.3)
1.9.3p194 :001 > product = Product.available.build
 => #<Product id: nil, name: nil, price: nil, weight: nil, in_stock: true, created_at: nil, updated_at: nil> 
1.9.3p194 :002 > product.in_stock
 => true 
1.9.3p194 :003 > exit
MacBook:shop xyz$
Das funktioniert bei der Methode build (siehe „build“) und create (siehe „create“).