find_or_initialize_by vs create_or_find_by
RecordNotUniqueが起きる理由 — findとcreateの順序
2つのタブで同じボタンをほぼ同時に押すとActiveRecord::RecordNotUniqueが爆発する。エラートラッカーにこれが記録されていたら、犯人はほぼ間違いなくfind_or_initialize_byかfind_or_create_byだ。
なぜ爆発するのか
find_or_initialize_byは先にSELECTを投げ、なければメモリ上でnewするだけ。INSERTは後でsaveを呼んだ時に実行される。問題はこの「探す」と「保存する」の間の隙間だ。
リクエストAがSELECTを投げる。ない。リクエストBもSELECTを投げる。AがまだINSERT前なのでBも「ない」と判断する。AのINSERTが成功すると、BのINSERTはユニークインデックスに引っかかって死ぬ。
find_or_create_byもfind → createの順序は同じなので、この隙間は消せない。bang(!)を付けても順序は変わらない。
create_or_find_byは順序を逆にする
SELECTなしでいきなりINSERTを試みる。既に存在してユニーク制約に引っかかったら、そのRecordNotUniqueを内部でrescueし、SELECTで既存レコードを取って返す。
存在判定をアプリケーションのSELECT結果ではなく、DBのユニーク制約に任せるということ。同時リクエストが来ても片方はINSERT成功、もう片方は既存レコードを受け取るので、エラーは外に漏れない。
前提が1つある
検索条件のカラムの組み合わせにユニークインデックスが必要だ。なければ衝突自体が起きず、重複行が静かに積もる。インデックスなしのcreate_or_find_byはただのcreateだ。
選ぶ基準は単純。フォームに渡す未保存オブジェクトが欲しいならfind_or_initialize_by、同時に作られうるレコードを安全に確保したいならcreate_or_find_by。
3つのメソッド比較
| 項目 | find_or_initialize_by | find_or_create_by | create_or_find_by |
|---|---|---|---|
| 順序 | SELECT → new | SELECT → INSERT | INSERT → 衝突時SELECT |
| 返却時の状態 | なければ未保存new_record | persisted | persisted |
| 同時リクエスト時 | saveでRecordNotUnique | createでRecordNotUnique | エラーなしで既存レコード返却 |
| 前提 | — | — | ユニークインデックス必須 |
レースコンディションのタイムライン
タブA: SELECT → なし
タブB: SELECT → なし (AがまだINSERT前なのでBにも見えない)
タブA: INSERT → 成功
タブB: INSERT → ユニークインデックス違反 → RecordNotUnique
create_or_find_byの中身は実質これ
def create_or_find_by(attrs)
transaction(requires_new: true) { create(attrs) }
rescue ActiveRecord::RecordNotUnique
find_by!(attrs)
end
requires_new: trueがsavepointを作るので、外側に既にトランザクションがあってもINSERT失敗が全体を汚染しない。PostgreSQLでは特に重要だ。
⚠ ユニークインデックスがなければ無意味
衝突が起きなければrescueもない。まず検索条件カラムの組み合わせにユニークインデックスを張ること。渡した属性だけで即INSERTするので、他のカラムに必須バリデーションがあればその時点で失敗する点も覚えておくこと。
キーポイント
find_or_initialize_byはSELECT後、なければnew — INSERTはsave時点で実行される
同時リクエストでは両方が「ない」と見て両方INSERT — 遅い方がRecordNotUniqueで死ぬ
find_or_create_by(!)もfind → create順なので同じように失敗する
create_or_find_byはINSERTが先 — ユニーク制約に当たったらrescueして既存レコードをSELECT
前提: 検索条件カラムにユニークインデックス必須 — なければ重複が静かに積もる
transaction(requires_new: true)のsavepointを使うので外側のトランザクション内でも安全だ
メリット
- ✓ 存在判定をDBのユニーク制約に任せるので同時リクエストでもエラーが漏れない
- ✓ 返却時点で常にpersisted状態が保証される
- ✓ savepoint(requires_new: true)なので外側のトランザクション内でも安全
デメリット
- ✗ ユニークインデックスがなければただのcreate — 重複が静かに積もる
- ✗ 渡した属性だけで即INSERTするため、他の必須カラムのバリデーションに引っかかりうる
- ✗ 衝突直後に相手トランザクションがロールバックするとfind_by!が見つけられない稀なケースがある