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を返すようにしていい気がします。変にテストとか通ってしまう可能性もあって、見落としがちになりそうです。