セキュリティ系の勉強、その他開発メモとか雑談. GithubはUnity触っていた頃ものがメイン Twitterフォローもよろしくです

【Rails5】find_by(nil)が先頭レコードを返す

find_by

find_byメソッドは、検索するカラムとその条件を指定してそのレコードのうち最初の一件を取得します。例えばfind_by(id: 1)ならidが1のレコードを返します。

find_by(nil) -> nil ???

使い方は正しくはありませんが、この使い方をするとnilが返ってきそうですが、実は先頭レコードが1件返ってきます。挙動としてはfirstと同じ感じ。こんな書き方は普段しませんが、カラムの指定を忘れ、paramsの値がnilであったりキー名をタイポしたりした際に、エラーになりません。

環境

ruby 2.5.3p105
Rails 5.2.3

テストに使用するモデル

$ bin/rails g model User name:string

Userモデルを作って、idとnameというカラムを持たせます。マイグレートしてください。ひとまず"Alice"という名前のUserを作っておきます。

発行されるSQL

User.find_by(name: "Alice")
User Load (0.7ms)  SELECT  `users`.* FROM `users` WHERE `users`.`name` = 'Alice' LIMIT 1
=> #<User id: 1, name: "aa", created_at: "2019-08-07 13:02:51", updated_at: "2019-08-07 13:02:51">


User.find_by("Alice")
User Load (0.6ms)  SELECT  `users`.* FROM `users` WHERE (Alice) LIMIT 1 #エラー


User.find_by(name:nil)
User Load (0.4ms)  SELECT  `users`.* FROM `users` WHERE `users`.`name` IS NULL LIMIT 1
=> nil


User.find_by(nil)
User Load (0.3ms)  SELECT  `users`.* FROM `users` LIMIT 1
=> #<User id: 1, name: "aa", created_at: "2019-08-07 13:02:51", updated_at: "2019-08-07 13:02:51">

なぜ

find_by

    def find_by(arg, *args)
      where(arg, *args).take
    rescue ::RangeError
      nil
    end

より、where(nil).takeが呼ばれます。

User.where(nil)
  User Load (0.5ms)  SELECT  `users`.* FROM `users` LIMIT 11
=> #<ActiveRecord::Relation [#<User id: 1, name: "aa", created_at: "2019-08-07 13:02:51", updated_at: "2019-08-07 13:02:51">, #<User id: 2, name: "Alice", created_at: "2019-08-07 13:24:35", updated_at: "2019-08-07 13:24:35">]>

より、where(nil)は条件なしで検索がかかるようです。
takelimit 1がかかるので、結果的に先頭のレコードが返って来る仕組みです。



github内でのissue

github.com
だいぶ昔に閉じられていますが、find_byはwhereのエイリアスだから、結局where(nil)の挙動の問題に帰結するよ。みたいな感じで閉じられています。where(nil)がそういう挙動なら、そりゃそうでしょ?ってお話でしょうか?

しかし、その一年後、いややっぱりおかしいよ。nilを返すべきだ。find_byの挙動はwhereの話と別にして考えるべきだ。みたいなことを言っている人が現れました。結局、何も変更がなかったようですが。
個人的にも、find_by(nil)nilを返すようにしていい気がします。変にテストとか通ってしまう可能性もあって、見落としがちになりそうです。