『パーフェクト Ruby on Rails 【増補改訂版】』を読んでいたら、検索条件を Scope で定義した場合とクラスメソッドで定義した場合の違いが書かれていた。クラスメソッドで定義するというのがよくわからなかったのでまとめてみる。
Scope とは
Scope は、モデルで検索条件をひとまとめにして名前をつけられる仕組み。
Scope の定義方法
書き方
class モデル名 < ApplicationRecord scope :スコープ名, -> { 検索条件 } end
例(パーフェクト Ruby on Rails 2-2-1 より抜粋)
class Book < ApplicationRecord scope :costly, -> { where("price > ?", 3000) } end
値段が 3000円より高い本を検索するcostly
という Scope を定義。
コントローラーなどからは、以下のようにして呼び出せる。
Book.costly
条件に該当するレコードがあれば、 ActiveRecord::Relation クラスの配列のようなオブジェクトが返ってくる。
rails コンソールで確かめてみる。
irb(main):001:0> Book.costly Book Load (0.2ms) SELECT "books".* FROM "books" WHERE (price > 3000) LIMIT ? [["LIMIT", 11]] => #<ActiveRecord::Relation [#<Book id: 4, name: "Book 4", published_on: "2019-08-24", price: 4000, created_at: "2020-08-30 00:09:21", updated_at: "2020-08-30 00:09:21">, #<Book id: 5, name: "Book 5", published_on: "2019-07-24", price: 5000, created_at: "2020-08-30 00:09:21", updated_at: "2020-08-30 00:09:21">]>
Book.where("price > ?", 3000)
と同じ結果になる。
Scope は、以下のように引数を渡すことも可能。
class Book < ApplicationRecord scope :lower, ->(price) { where("price < ?", price) } end
引数price
より価格が低い本を検索する。呼び出すときは、Book.lower(3000)
のように引数を指定する。
irb(main):001:0> Book.lower(3000) (1.1ms) SELECT sqlite_version(*) Book Load (0.3ms) SELECT "books".* FROM "books" WHERE (price < 3000) LIMIT ? [["LIMIT", 11]] => #<ActiveRecord::Relation [#<Book id: 1, name: "Book 1", published_on: "2019-11-24", price: 1000, created_at: "2020-08-30 00:09:21", updated_at: "2020-08-30 00:09:21">, #<Book id: 2, name: "Book 2", published_on: "2019-10-24", price: 2000, created_at: "2020-08-30 00:09:21", updated_at: "2020-08-30 00:09:21">]>
クラスメソッドでの定義方法
上に書いた Scope lower
を、クラスメソッドで定義すると以下のようになる。
class Book < ApplicationRecord def self.lower(price) where("price < ?", price) end end
呼び出し方は、Scope のときと同じ。結果も同じ。
irb(main):001:0> Book.lower(3000) (1.1ms) SELECT sqlite_version(*) Book Load (0.4ms) SELECT "books".* FROM "books" WHERE (price < 3000) LIMIT ? [["LIMIT", 11]] => #<ActiveRecord::Relation [#<Book id: 1, name: "Book 1", published_on: "2019-11-24", price: 1000, created_at: "2020-08-30 00:09:21", updated_at: "2020-08-30 00:09:21">, #<Book id: 2, name: "Book 2", published_on: "2019-10-24", price: 2000, created_at: "2020-08-30 00:09:21", updated_at: "2020-08-30 00:09:21">]>
Scope とクラスメソッドの違い
スコープでのメソッドの設定は、クラスメソッドの定義と完全に同じ (というよりクラスメソッドの定義そのもの) です。どちらの形式を使用するかは好みの問題です。
〜 中略 〜
ただし1つ注意点があります。それは条件文を評価した結果がfalse
になった場合であっても、スコープは常にActiveRecord::Relation
オブジェクトを返すという点です。クラスメソッドの場合はnil
を返すので、この振る舞いが異なります。したがって、条件文を使ってクラスメソッドをチェインさせていて、かつ、条件文のいずれかがfalse
を返す場合、NoMethodError
を発生することがあります。
Active Record クエリインターフェイス - Railsガイド
Scope の方が短く書けるため、複雑でない条件であれば Scope が使われることが多そう。
Scope とクラスメソッドは基本的に同じだが、Scope は必ず ActiveRecord::Relation クラスのオブジェクトを返す点に注意が必要。nil
を返すことはない。
例(パーフェクト Ruby on Rails 2-2-1 より抜粋)
class Book < ApplicationRecord scope :find_price, ->(price) { find_by(price: price) } end
指定した価格の本を検索する Scope。
この Scope と同じ動きをする(はずの)クラスメソッドfind_by_price
を定義して、比べてみる。
class Book < ApplicationRecord scope :find_price, ->(price) { find_by(price: price) } def self.find_by_price(price) find_by(price: price) end end
該当するレコードがあるとき
該当するレコードが存在するときは、以下のように、Scope でもクラスメソッドでも同じ結果となる。
Scopeの場合
irb(main):001:0> Book.find_price(2000) (1.1ms) SELECT sqlite_version(*) Book Load (0.5ms) SELECT "books".* FROM "books" WHERE "books"."price" = ? LIMIT ? [["price", 2000], ["LIMIT", 1]] => #<Book id: 2, name: "Book 2", published_on: "2019-10-24", price: 2000, created_at: "2020-08-30 00:09:21", updated_at: "2020-08-30 00:09:21">
クラスメソッドの場合
irb(main):002:0> Book.find_by_price(2000) Book Load (0.2ms) SELECT "books".* FROM "books" WHERE "books"."price" = ? LIMIT ? [["price", 2000], ["LIMIT", 1]] => #<Book id: 2, name: "Book 2", published_on: "2019-10-24", price: 2000, created_at: "2020-08-30 00:09:21", updated_at: "2020-08-30 00:09:21">
該当するレコードがないとき
該当するレコードが存在しないときは、以下のように結果が異なる。
Scopeの場合
irb(main):001:0> Book.find_price(1500) (1.2ms) SELECT sqlite_version(*) Book Load (0.3ms) SELECT "books".* FROM "books" WHERE "books"."price" = ? LIMIT ? [["price", 1500], ["LIMIT", 1]] Book Load (0.1ms) SELECT "books".* FROM "books" LIMIT ? [["LIMIT", 11]] => #<ActiveRecord::Relation [#<Book id: 1, name: "Book 1", published_on: "2019-11-24", price: 1000, created_at: "2020-08-30 00:09:21", updated_at: "2020-08-30 00:09:21">, #<Book id: 2, name: "Book 2", published_on: "2019-10-24", price: 2000, created_at: "2020-08-30 00:09:21", updated_at: "2020-08-30 00:09:21">, #<Book id: 3, name: "Book 3", published_on: "2019-09-24", price: 3000, created_at: "2020-08-30 00:09:21", updated_at: "2020-08-30 00:09:21">, #<Book id: 4, name: "Book 4", published_on: "2019-08-24", price: 4000, created_at: "2020-08-30 00:09:21", updated_at: "2020-08-30 00:09:21">, #<Book id: 5, name: "Book 5", published_on: "2019-07-24", price: 5000, created_at: "2020-08-30 00:09:21", updated_at: "2020-08-30 00:09:21">]>
クラスメソッドの場合
irb(main):002:0> Book.find_by_price(1500) Book Load (0.2ms) SELECT "books".* FROM "books" WHERE "books"."price" = ? LIMIT ? [["price", 1500], ["LIMIT", 1]] => nil
クラスメソッドは nil
を返すが、Scope の場合はメソッドチェーンを続けられる。
結果が nil となった場合は該当 Scope の検索条件を除外したクエリを発行し、必ず ActiveRecord::Relation を返すという動作をします。
パーフェクト Ruby on Rails 【増補改訂版】
コントローラーで検索しない
コントローラーやビューに検索条件を書くことも可能ではあるが、望ましくないようだ。詳しくはこちらの記事。
(その他参考サイト)