scopeのメリット
- 繰り返し利用するクエリの再利用性が上がる
- クエリに名前を付けることで、可読性が向上する
scopeの注意点
scopeの結果がnil
となった場合は、nil
を返す該当scopeの検索条件を除外したクエリを発行し、必ずActiveRecord::Relationを返すという動作が行われる
本記事は、こちらの注意点にフォーカスします。
(先に)結論
nil
を返す可能性がある場合にはscopeで定義せず、クラスメソッドで定義する
動作確認のための準備
環境
- Rubyバージョン:
2.6.6
- Railsバージョン:
6.0.3
モデルの作成
rails g model Book book_title:string publication_date:date price:integer
マイグレーション
rails db:migrate
scopeを定義
class Book < ApplicationRecord
scope :search_by_title, ->(book_title) { where('book_title like ?', "%#{book_title}%") } # 通常のscope
scope :find_by_price, ->(price) { find_by(price: price) } # nilを返す可能性のあるscope
end
任意のレコードを作成
(base) sample@SamplenoMBP practice % rails c
Loading development environment (Rails 6.0.3)
irb(main):001:0> (1..5).each { |i| Book.create(book_title: "Book #{i}", publication_date: Time.parse('20240901').ago(i.months), price: (i * 1000)) }
(0.8ms) SELECT sqlite_version(*)
(0.0ms) begin transaction
Book Create (1.0ms) INSERT INTO "books" ("book_title", "publication_date", "price", "created_at", "updated_at") VALUES (?, ?, ?, ?, ?) [["book_title", "Book 1"], ["publication_date", "2024-08-01"], ["price", 1000], ["created_at", "2024-08-31 09:10:12.130910"], ["updated_at", "2024-08-31 09:10:12.130910"]]
(0.6ms) commit transaction
(0.0ms) begin transaction
Book Create (0.2ms) INSERT INTO "books" ("book_title", "publication_date", "price", "created_at", "updated_at") VALUES (?, ?, ?, ?, ?) [["book_title", "Book 2"], ["publication_date", "2024-07-01"], ["price", 2000], ["created_at", "2024-08-31 09:10:12.138869"], ["updated_at", "2024-08-31 09:10:12.138869"]]
(0.3ms) commit transaction
(0.0ms) begin transaction
Book Create (0.2ms) INSERT INTO "books" ("book_title", "publication_date", "price", "created_at", "updated_at") VALUES (?, ?, ?, ?, ?) [["book_title", "Book 3"], ["publication_date", "2024-06-01"], ["price", 3000], ["created_at", "2024-08-31 09:10:12.139852"], ["updated_at", "2024-08-31 09:10:12.139852"]]
(0.3ms) commit transaction
(0.0ms) begin transaction
Book Create (0.2ms) INSERT INTO "books" ("book_title", "publication_date", "price", "created_at", "updated_at") VALUES (?, ?, ?, ?, ?) [["book_title", "Book 4"], ["publication_date", "2024-05-01"], ["price", 4000], ["created_at", "2024-08-31 09:10:12.140731"], ["updated_at", "2024-08-31 09:10:12.140731"]]
(0.3ms) commit transaction
(0.0ms) begin transaction
Book Create (0.2ms) INSERT INTO "books" ("book_title", "publication_date", "price", "created_at", "updated_at") VALUES (?, ?, ?, ?, ?) [["book_title", "Book 5"], ["publication_date", "2024-04-01"], ["price", 5000], ["created_at", "2024-08-31 09:10:12.141575"], ["updated_at", "2024-08-31 09:10:12.141575"]]
(0.3ms) commit transaction
敢えてnilが返る条件でscopeを実行
nilを返す(可能性のある)scopeのみの場合
irb(main):001:0> Book.find_by_price(10000)
Book Load (0.3ms) SELECT "books".* FROM "books" WHERE "books"."price" = ? LIMIT ? [["price", 10000], ["LIMIT", 1]]
Book Load (0.1ms) SELECT "books".* FROM "books" LIMIT ? [["LIMIT", 11]]
=> #<ActiveRecord::Relation [#<Book id: 1, book_title: "Book 1", publication_date: "2024-08-01", price: 1000, created_at: "2024-08-31 09:10:12", updated_at: "2024-08-31 09:10:12">, #<Book id: 2, book_title: "Book 2", publication_date: "2024-07-01", price: 2000, created_at: "2024-08-31 09:10:12", updated_at: "2024-08-31 09:10:12">, #<Book id: 3, book_title: "Book 3", publication_date: "2024-06-01", price: 3000, created_at: "2024-08-31 09:10:12", updated_at: "2024-08-31 09:10:12">, #<Book id: 4, book_title: "Book 4", publication_date: "2024-05-01", price: 4000, created_at: "2024-08-31 09:10:12", updated_at: "2024-08-31 09:10:12">, #<Book id: 5, book_title: "Book 5", publication_date: "2024-04-01", price: 5000, created_at: "2024-08-31 09:10:12", updated_at: "2024-08-31 09:10:12">]>
該当scopeの条件によって、取得された結果が本来nil
となる場合
→nil
になる該当scopeの条件を除いたクエリが発行される
該当scopeの条件を除いた場合、このケースだとBook.all
と同等になる為、全てのレコードが返る
通常のscope + nilを返す可能性のあるscope
irb(main):002:0> Book.search_by_title('Book 1').find_by_price(10000)
(1.3ms) SELECT sqlite_version(*)
Book Load (0.3ms) SELECT "books".* FROM "books" WHERE (book_title like '%Book 1%') AND "books"."price" = ? LIMIT ? [["price", 10000], ["LIMIT", 1]]
Book Load (0.1ms) SELECT "books".* FROM "books" WHERE (book_title like '%Book 1%') LIMIT ? [["LIMIT", 11]]
=> #<ActiveRecord::Relation [#<Book id: 1, book_title: "Book 1", publication_date: "2024-08-01", price: 1000, created_at: "2024-08-31 09:10:12", updated_at: "2024-08-31 09:10:12">]>
該当scopeの条件を除いた、
SELECT books.* FROM books WHERE (book_title like '%Book 1%')
の結果(今回の場合だと1件)が返る
本来nil
を期待しているとすると、想定しない結果が返ることになる
通常scope(検索結果なし) + nilを返すscopeの場合
irb(main):004:0> Book.search_by_title('zzz').find_by_price(10000)
Book Load (0.7ms) SELECT "books".* FROM "books" WHERE (book_title like '%zz%') AND "books"."price" = ? LIMIT ? [["price", 10000], ["LIMIT", 1]]
Book Load (0.3ms) SELECT "books".* FROM "books" WHERE (book_title like '%zz%') LIMIT ? [["LIMIT", 11]]
=> #<ActiveRecord::Relation []>
該当のscopeの条件を除いた、
SELECT books.* FROM books WHERE (book_title like '%zz%')
が実行される。
ActiveRecord
に対してQuery Interface
を呼び出しているため、
通常通り「空のActiceRecord::Relation」インスタンスが返る
nilを返す可能性のあるscopeが、nilを返さない場合
想定通り動作する
irb(main):003:0> Book.where(id: [1,2,3]).find_by_price(2000)
Book Load (1.0ms) SELECT "books".* FROM "books" WHERE "books"."id" IN (?, ?, ?) AND "books"."price" = ? LIMIT ? [["id", 1], ["id", 2], ["id", 3], ["price", 2000], ["LIMIT", 1]]
=> #<Book id: 2, book_title: "Book 2", publication_date: "2024-07-01", price: 2000, created_at: "2024-08-31 09:10:12", updated_at: "2024-08-31 09:10:12">
nil
を返さない場合は、想定通りの結果を返す
しかし、nil
が返る可能性がある時点でscopeとして定義するのは良くない
nilが想定される場合は、クラスメソッドで定義する
class Book < ApplicationRecord
def self.find_by_price(price)
find_by(price: price)
end
end
irb(main):001:0> Book.find_by_price(10000)
Book Load (0.4ms) SELECT "books".* FROM "books" WHERE "books"."price" = ? LIMIT ? [["price", 10000], ["LIMIT", 1]]
=> nil
想定通り、nil
が返却される