はじめに
Railsで開発している時、scopeを使うシーンは多いと思います。
しかし、実務で使用している際、予期しない動作をすることがあったので、調べたことをまとめておきます。
実行環境
- macOS Catalina 10.15.7
- ruby 2.6.6
- Rails 5.2.4.3
事前準備
今回はnameというカラムを持つ、Userというモデルを使用していきます。
また、hogeという名前とfugaという名前を持つ2つのレコードが入っている想定です。
> User.all
=> User Load (1.1ms) SELECT `users`.* FROM `users`
# <User id: 1, name: "hoge">,
# <User id: 2, name: "fuga">
scopeでUserを検索する
まずは通常通りscopeでUserを検索してみましょう。
scope自体の使い方の説明が主目的ではないので、簡単に特定の名前で検索するscopeを考えます。
class User < ApplicationRecord
scope :find_hoge, -> () { find_by(name: "hoge") }
end
このscopeを使うことで、nameがhogeであるuserを検索することができます。
発行されるsqlをみても、きちんとnameがhogeであるレコードを検索できていることが分かるかと思います。
> User.find_hoge
User Load (0.8ms) SELECT `users`.* FROM `users` WHERE `users`.`name` = 'hoge' LIMIT 1
=> #<User: id: 1, name: "hoge">
scopeでnilを返してみる
さて、それでは次に結果がnilを返すscopeを作成してみます。
class User < ApplicationRecord
...
scope :find_nobody, -> () { find_by(name: "nobody") }
end
上記のscopeではnameがnobodyであるレコードを検索していますが、該当するレコードが無いためnilを返すと思われます。
実際に実行してみましょう。
> User.find_nobody
User Load (1.4ms) SELECT `users`.* FROM `users` WHERE `users`.`name` = 'nobody' LIMIT 1
=> User Load (1.2ms) SELECT `users`.* FROM `users`
# <User id: 1, name: "hoge">,
# <User id: 2, name: "fuga">
あれ?sqlが2回実行されていますね。
しかもallをした時と同様の挙動をしてしまっています。
どうやら結果もnilではないようです。
> User.find_nobody.nil?
User Load (1.0ms) SELECT `users`.* FROM `users` WHERE `users`.`name` = 'nobody' LIMIT 1
=> false
原因
この挙動の原因は、scopeの仕様にあります。
scopeはメソッドチェインなどで複数の条件を結合して使用することが想定されているため、結果がnilとなった場合は該当scopeの検索条件を除外したクエリを発行するという仕様になっているようです。
今回のケースではscopeの中身を直接実行してみると、想定通りnilを返していることが分かります。
> User.find_by(name: "nobody")
User Load (1.2ms) SELECT `users`.* FROM `users` WHERE `users`.`name` = 'nobody' LIMIT 1
=> nil
そのため、find_nobodyの条件を除外したクエリ、つまり検索条件の無いallの結果が返ってきてしまっているのです。
実際に、scopeを連結してみると、上記の仕様がよく分かるかと思います。
find_nobodyとfind_hogeをチェインして使用してみましょう。
> User.find_nobody.find_hoge
User Load (3.9ms) SELECT `users`.* FROM `users` WHERE `users`.`name` = 'nobody' LIMIT 1
User Load (2.0ms) SELECT `users`.* FROM `users` WHERE `users`.`name` = 'hoge' LIMIT 1
=> #<User: id: 1, name: "hoge">
nobodyについて検索した後、hogeのみの条件で検索するクエリが発行されていますね。
対応策
ここまで見てきたように、nilを返す可能性がある場合はscopeで実装してしまうと予期しない結果となることがあるので、クラスメソッドで実装しておくのが良いかもしれません。
class User < ApplicationRecord
...
...
def self.find_nobody
find_by(name: "nobody")
end
end
> User.find_nobody
User Load (0.7ms) SELECT `users`.* FROM `users` WHERE `users`.`name` = 'nobody' LIMIT 1
=> nil
無事にnilが返ってきますね。
まとめ
scopeを使うことで頻繁に使用する処理などをまとめておけるのが便利です。
しかし、nilを返す場合には今回紹介したように想定と異なる動作をする場合があるので、注意する必要がありそうです。