🔀

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 → newSELECT → INSERTINSERT → 衝突時SELECT
返却時の状態なければ未保存new_recordpersistedpersisted
同時リクエスト時saveでRecordNotUniquecreateで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するので、他のカラムに必須バリデーションがあればその時点で失敗する点も覚えておくこと。

キーポイント

1

find_or_initialize_byはSELECT後、なければnew — INSERTはsave時点で実行される

2

同時リクエストでは両方が「ない」と見て両方INSERT — 遅い方がRecordNotUniqueで死ぬ

3

find_or_create_by(!)もfind → create順なので同じように失敗する

4

create_or_find_byはINSERTが先 — ユニーク制約に当たったらrescueして既存レコードをSELECT

5

前提: 検索条件カラムにユニークインデックス必須 — なければ重複が静かに積もる

6

transaction(requires_new: true)のsavepointを使うので外側のトランザクション内でも安全だ

メリット

  • 存在判定をDBのユニーク制約に任せるので同時リクエストでもエラーが漏れない
  • 返却時点で常にpersisted状態が保証される
  • savepoint(requires_new: true)なので外側のトランザクション内でも安全

デメリット

  • ユニークインデックスがなければただのcreate — 重複が静かに積もる
  • 渡した属性だけで即INSERTするため、他の必須カラムのバリデーションに引っかかりうる
  • 衝突直後に相手トランザクションがロールバックするとfind_by!が見つけられない稀なケースがある

ユースケース

ユーザーごとの設定/フィルタなど1ユーザー1件レコードのupsert いいね・ブックマークなど重複作成禁止のトグル系レコード タグ・マスターデータを名前基準で自動作成 同じイベントが2回来ても冪等であるべきWebhook受信処理