ActiveRecord::Relationのfind_or_initialize_byやfind_or_create_by!は並列実行に気をつける
find_or_initialize_byは便利だが、初期化するのとsave!するのとが別個に行われていることに注意が必要である。
たとえばアプリをマルチプロセスで動かしている場合、ユニーク制約がかかっているプロパティについて同じ値をもったインスタンスがふたつ初期化されてしまい、どちらか片方が先にsave!されることで後になった方がActiveRecord::RecordNotUniqueになってしまうことがある。
find_or_create_by!も同様の話がいえる。
Rails 6からはcreate_or_find_byというこの問題を回避するためのメソッドが用意されている、が……。
MySQLやPostgreSQLではcreate(INSERT)を試す時点でidが繰り上がってしまい、idの値がすぐ膨らんでしまう問題がある。
対策1:
Modelでuniquenessのvalidationをして、レコードが作られなかった場合を拾う。
これならcreate_or_find_byしても先にvalidationのチェックが走るのでidの値が膨らんでしまう問題を回避できる。
ただし、ActiveRecord::RecordInvalidを拾う必要はある。
対策2:
ActiveRecord::RecordNotUniqueをrescueする。
code:ruby
begin
foo = Foo.find_or_create_by!(id: id)
rescue ActiveRecord::RecordNotUnique
foo = Foo.find_by!(id: id)
end
ユニーク制約以外の部分をfindさせると失敗することがあるので注意。
foo = Foo.find_or_create_by!(id: id, piyo: piyo)は、piyoの内容が違うときに「findでは見つからないけど同じidのレコードが既にある」状態になる。
ユニーク制約がある部分だけ先にfind_or_initialize_byして、save!するところでActiveRecord::RecordNotUniqueを調べる。
Modelでuniquenessのvalidationをしている場合はそのままだと使えないので注意。
ActiveRecord::RecordNotUniqueより先にActiveRecord::RecordInvalidが投げられるため。
対策3:
ロックを使う。