この記事で書くこと
Railsガイド | find_or_initialize_by の挙動で、驚いたので忘れないように残す。
具体的には、 find_or_initialize_by
の引数の値がnilだった時の挙動について書きます。
例示
params[:name]
には nil
が入る可能性がある。
User.name
にはuniq制約がついている。
user = User.find_or_initialize_by(name: params[:name])
この時、どういったsqlが発行されるのか?
params[:name] がnilではなく、nameが引数のnameのレコードが既に存在している場合
# params[:name] は `test_user` だとする。
User Load (1.1ms) SELECT `users`.* FROM `users` WHERE `users`.`name` = 'test_user' LIMIT 1
存在するレコードが返る。
params[:name] がnilではなく、nameが引数のnameのレコードが存在していない場合
# params[:name] は `hoge` だとする。
User Load (2.7ms) SELECT `users`.* FROM `users` WHERE `users`.`name` = 'hoge' LIMIT 1
発行されるsqlは同じだが、返る値は、idを持たない、user.name
がhoge
のレコード。
params[:name] がnilの場合
# params[:name] は `nil` だとする。
User Load (2.7ms) SELECT `users`.* FROM `users` WHERE `users`.`name` IS NULL LIMIT 1
当たり前と言っては当たり前なのだが、name
に値が入ってないレコードの一件目が返る。
勝手にnilが入ったときには、エラーになるとか、既存のレコードは返却されずに新しいインスタンスが作成されて返されるとか都合の良い解釈を期待するが、そんなことは幻想。
全く意図しないレコードを更新してしまう可能性がある。
find_or_initialize_by
の引数に意図せずnilが入る可能性がある処理を書くことは危ないことがわかった
実装の中身を読んでみると、
# File activerecord/lib/active_record/relation.rb, line 226
def find_or_initialize_by(attributes, &block)
find_by(attributes) || new(attributes, &block)
end
とあり、まず、 nil
だって立派な値なのだから find_by
にだってしっかり入っていく。というのが理解できた。
find_or_create_by
も同じく注意が必要。
# 意図してないけど、nilが入ってしまった。。。。。。。
new = User.find_or_create_by(name: nil)
User Load (5.3ms) SELECT `users`.* FROM `users` WHERE `users`.`name` IS NULL LIMIT 1
まとめ
「えっ!!何か知らないレコードが勝手に更新されてるんですが!!!!!」
を無くすためには、頭の中の勝手な都合の良い解釈をやめ、挙動をしっかり確認しよう。
nilは意図して使いたいとき以外は引数に入れないようにしようと思います。