【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)は条件なしで検索がかかるようです。
take
はlimit 1
がかかるので、結果的に先頭のレコードが返って来る仕組みです。
github内でのissue
github.com
だいぶ昔に閉じられていますが、find_byはwhereのエイリアスだから、結局where(nil)の挙動の問題に帰結するよ。みたいな感じで閉じられています。where(nil)がそういう挙動なら、そりゃそうでしょ?ってお話でしょうか?
しかし、その一年後、いややっぱりおかしいよ。nil
を返すべきだ。find_by
の挙動はwhereの話と別にして考えるべきだ。みたいなことを言っている人が現れました。結局、何も変更がなかったようですが。
個人的にも、find_by(nil)
はnilを返すようにしていい気がします。変にテストとか通ってしまう可能性もあって、見落としがちになりそうです。