0
0

【Rails】scopeの注意点

Last updated at Posted at 2024-08-31

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を定義

app/models/book.rb
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

任意のレコードを作成

rails c
(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のみの場合

rails c
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

rails c
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の場合

rails c
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を返さない場合

想定通り動作する

rails c
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が想定される場合は、クラスメソッドで定義する

app/models/book.rb
class Book < ApplicationRecord
  def self.find_by_price(price)
    find_by(price: price)
  end
end
rails c
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が返却される

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0