モデルにはScopeというものを定義できます。Scopeとは、よく利用する検索条件に名前を付けるひとまとめにしたものです。
繰り返し利用するクエリの再利用性が上がる
クエリに名前を付けることで、可読性が向上する
class Book < ApplicationRecord
scope :costly, -> {where("price > ?", 3000)}
end
irb(main):001:0> Book.costly
TRANSACTION (0.2ms) BEGIN
Book Load (20.9ms) SELECT `books`.* FROM `books` WHERE (price > '3000')
=>
[#<Book:0x000000010d930158
id: 4,
name: "Book 4",
published_on: Thu, 08 Aug 2024,
price: 4000,
created_at: Sun, 08 Dec 2024 10:04:21.547588000 UTC +00:00,
updated_at: Sun, 08 Dec 2024 10:04:21.547588000 UTC +00:00>,
#<Book:0x000000010d97bfb8
id: 5,
name: "Book 5",
published_on: Mon, 08 Jul 2024,
price: 5000,
created_at: Sun, 08 Dec 2024 10:04:21.549149000 UTC +00:00,
updated_at: Sun, 08 Dec 2024 10:04:21.549149000 UTC +00:00>]
3000より大きいものが表示された
スコープをつなげることによって読んだだけで返り血の内容が理解できる
このように定義しておけば、Book.costly.written_about("java")という記述が可能になり、コードを見るだけで「高価」で「Javaについて書かれた」本を探している、ということがより明確になります。
Query Interface(find_byなど)を使ってスコープを作成できる。
:written_aboutScopeのように、特定のパラメータを利用するスコープも定義可能なことを覚えておいてください。Scopeを定義した際の返り値は、先ほどのActiveRecord::Relationとなるので、Scopeのチェインだけでなく、通常のQuery Interfaceを呼び出すこともできる。
クラスメソッドとスコープの違い
クラスメソッドで定義した場合との違いについて説明します。
ここまでのScopeの解説では、クラスメソッドで任意の探索条件を書いた場合とほとんど同じではと感じると思います。多くの場合は、クラスメソッドで条件を記述することでショートハンドとしてScopeを活用できます。しかし次のような場合は違いが現れます。
次のScopeはfind_byを利用しているため、条件がマッチしなければnilが返ることを想定しています。
class Book < ApplicationRecord
scope :costly, -> {where("price > ?", 3000)}
scope :written_about, ->(theme) {where("name like ?", "%#{theme}")}
scope :find_price, ->(price) {find_by(price: price)}
def self.find_price2(price)
self.find_by(price: price)
end
end
Scopeはないものを呼び出すと意図したものは返ってこない。しかしクラスメソッドはnilが返ってくる
irb(main):002:0> Book.find_price2(10000)
TRANSACTION (0.2ms) BEGIN
Book Load (0.5ms) SELECT `books`.* FROM `books` WHERE `books`.`price` = 10000 LIMIT 1
=> nil
irb(main):003:0> Book.find_price(10000)
Book Load (0.4ms) SELECT books`.* FROM `books` WHERE `books`.`price` = 10000 LIMIT 1
Book Load (0.4ms) SELECT books`.* FROM `books`
=>
[#<Book:0x000000010bd393f0
id: 1,
name: "Book 1",
published_on: Fri, 08 Nov 2024,
price: 1000,
created_at: Sun, 08 Dec 2024 10:04:21.535099000 UTC +00:00,
updated_at: Sun, 08 Dec 2024 10:04:21.535099000 UTC +00:00>,
#<Book:0x000000010ad51d48
id: 2,
name: "Book 2",
published_on: Tue, 08 Oct 2024,
price: 2000,
created_at: Sun, 08 Dec 2024 10:04:21.542065000 UTC +00:00,
updated_at: Sun, 08 Dec 2024 10:04:21.542065000 UTC +00:00>,
#<Book:0x000000010ad51c80
id: 3,
name: "Book 3",
published_on: Sun, 08 Sep 2024,
price: 3000,
created_at: Sun, 08 Dec 2024 10:04:21.545182000 UTC +00:00,
updated_at: Sun, 08 Dec 2024 10:04:21.545182000 UTC +00:00>,
#<Book:0x000000010ad51bb8
id: 4,
name: "Book 4",
published_on: Thu, 08 Aug 2024,
price: 4000,
created_at: Sun, 08 Dec 2024 10:04:21.547588000 UTC +00:00,
updated_at: Sun, 08 Dec 2024 10:04:21.547588000 UTC +00:00>,
#<Book:0x000000010ad51af0
id: 5,
name: "Book 5",
published_on: Mon, 08 Jul 2024,
price: 5000,
created_at: Sun, 08 Dec 2024 10:04:21.549149000 UTC +00:00,
updated_at: Sun, 08 Dec 2024 10:04:21.549149000 UTC +00:00>]
テーブルに存在すると意図した通りに返ってくる
irb(main):005:0> Book.find_price(1000)
Book Load (0.4ms) SELECT `books`.* FROM `books` WHERE `books`.`price` = 1000 LIMIT 1
=>
#<Book:0x000000010b932590
id: 1,
name: "Book 1",
published_on: Fri, 08 Nov 2024,
price: 1000,
created_at: Sun, 08 Dec 2024 10:04:21.535099000 UTC +00:00,
updated_at: Sun, 08 Dec 2024 10:04:21.535099000 UTC +00:00>
ScopeはActiveRecord_Relationのクラスを返す
irb(main):007:0> a = Book.written_about("jova").find_price(1000)
Book Load (0.5ms) SELECT `books`.* FROM `books` WHERE (name like '%jova') AND `books`.`price` = 1000 LIMIT 1
Book Load (0.5ms) SELECT `books`.* FROM `books` WHERE (name like '%jova')
=> []
irb(main):008:0> a.class
=> Book::ActiveRecord_Relation
デフォルトスコープは複雑だと意図しない挙動を起こす
default_scope(scope = nil, all_queries: nil, &block)
Use this macro in your model to set a default scope for all operations on the model.
class Article < ActiveRecord::Base
default_scope { where(published: true) }
end
Article.all
# SELECT * FROM articles WHERE published = true
default_scopeはそのモデルクラスでの操作すべてに影響を与えるため、複雑な条件での検索や別軸で探索したい場合に意図しない挙動となる場合もあります