2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Ruby on Rails: scopeはnilを返す使い方をしてはいけない

2
Posted at

はじめに

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を使うことで、namehogeであるuserを検索することができます。
発行されるsqlをみても、きちんとnamehogeであるレコードを検索できていることが分かるかと思います。

> 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ではnamenobodyであるレコードを検索していますが、該当するレコードが無いため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_nobodyfind_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を返す場合には今回紹介したように想定と異なる動作をする場合があるので、注意する必要がありそうです。

参考

2
2
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
2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?