`create_or_find_by` のリトライ時にWHERE条件が重複するバグを修正

rails/rails

create_or_find_byRecordNotUnique エラーをキャッチしてリトライする際、既存のスコープ条件と検索条件が重複して RecordNotFound エラーが発生するバグが修正されました。whererewhere に、find_by!take! に置き換えることで、スコープ汚染を防ぎます。

背景

create_or_find_by のリトライロジックに、既存スコープと検索条件が競合するバグが存在していました。#57192 で報告されたこの問題は、CollectionProxy などで事前にスコープが設定されている場合に顕在化します。

具体的には、where(from_slug: "new-slug") でスコープされたリレーションに対して create_or_find_by(from_slug: "old-slug") を呼び出した場合、リトライ時のクエリが WHERE from_slug = 'new-slug' AND from_slug = 'old-slug' という矛盾した条件を生成していました。old-slug のレコードが存在しているにもかかわらず、この矛盾条件により find_by! が一致するレコードを見つけられず、RecordNotFound エラーが発生していました。

この状況は has_manyCollectionProxy を経由して create_or_find_by を呼び出す際に特に発生しやすく、実際のアプリケーションでのユースケースで問題となっていました。

技術的な変更

activerecord/lib/active_record/relation.rbcreate_or_find_by および create_or_find_by! のリトライパスが変更されました。変更は where(attributes).lock.find_by!(attributes)rewhere(attributes).lock.take! に置き換える2行のみです。

変更前:

rescue ActiveRecord::RecordNotUnique
  if connection.transaction_open?
    where(attributes).lock.find_by!(attributes)
  else
    find_by!(attributes)
  end

変更後:

rescue ActiveRecord::RecordNotUnique
  if connection.transaction_open?
    rewhere(attributes).lock.take!
  else
    find_by!(attributes)
  end

whererewhere への変更が核心です。where は既存のスコープ条件を保持したまま条件を追加しますが、rewhere は同一カラムへの既存条件を上書きします。これにより、nick: "alice" のようなスコープが設定済みのリレーションに対して rewhere(nick: "bob") を呼ぶと、nick: "alice" の条件が nick: "bob" に置き換わり、矛盾した条件の生成を防ぎます。

find_by!(attributes)take! への変更も重要です。rewhere(attributes) によって検索条件はリレーションのスコープに組み込まれているため、改めて find_by!(attributes) に同じ条件を渡す必要がなくなりました。take! はスコープに設定されたWHERE条件をそのまま使用してレコードを1件取得します。

テストでは test_find_or_create_by_race_condition のスタブ対象が :find_by! から :take! に変更され、新たに test_create_or_find_by_with_polluted_scopetest_create_or_find_by_bang_with_polluted_scope の2つのテストケースが追加されました。これらは where(nick: "alice") でスコープされたリレーションから create_or_find_by(nick: "bob") を呼び出し、正しくレコードが返ることを検証します。

設計判断

rewhere を使った条件の上書きというアプローチが採用されました。

リトライ時のクエリ構築において、create_or_find_by に渡された attributes は「このレコードを一意に識別する条件」です。呼び出し元のスコープに同一カラムへの条件が含まれている場合、それは create_or_find_by の意図と競合します。rewhere によって attributes のカラムに関する既存条件を上書きすることで、「渡された属性でレコードを特定する」という意図を確実に実現しています。

トランザクション外のパス(else 節)の find_by!(attributes) は変更されていません。これはトランザクション外ではロックが不要なため、既存の実装がそのまま維持されています。トランザクション内のパスのみを最小限に変更することで、影響範囲を絞り込んだ修正となっています。

まとめ

本PRは、create_or_find_by のリトライロジックにおける wherefind_by! の組み合わせが引き起こすスコープ汚染を、rewheretake! への置き換えで根本的に解消した変更です。変更行数は2行と最小限ながら、CollectionProxy 経由のユースケースで発生していた RecordNotFound エラーを確実に修正し、重複条件の生成というアンチパターンを排除しています。

記事メタデータ

Generated by:
Claude Sonnet 4.6 for DiffDaily
LLM Trace:
26f13b20

この記事はAIによって自動生成されています。内容の正確性については、必ずソースコードやPRを確認してください。

品質レビュー結果

Review Status:
承認済み
Review Count:
1回
Reviewed by:
Gemini 2.5 Pro for DiffDaily

Review Criteria:

記事構成 ✓ PASS

Title, Context, Technical Detailの存在と明確さ

「リード文(総論)→背景・技術詳細・設計判断(各論)→まとめ(結論)」という3部構成が明確に適用されており、非常に理解しやすい構造です。

カスタムMarkdown構文 ✓ PASS

シンタックスハイライト・GitHubリンク記法の正確性

ファイル名付きシンタックスハイライト(```ruby:path```)やGitHubのPR/Issueリンク記法([#123](URL))が正しく使用されています。

対象読者への適合性 ✓ PASS

エンジニア向けの適切な技術レベルと表現

専門用語が適切に使用され、前提知識を持つエンジニアを対象とした適切な技術レベルで記述されています。

パラグラフ・ライティング ✓ PASS

トピックセンテンス・1段落1トピック・段落長

各セクションが総論→各論の構成になっており、かつ各パラグラフがトピックセンテンスで始まるなど、パラグラフ・ライティングの原則が非常によく守られています。

Diff内容との照合 ✓ PASS

コードブロックとDiff内容の一致

記事で引用されている`activerecord/lib/active_record/relation.rb`のコード変更は、提供されたDiff情報と完全に一致しています。

技術用語の正確性 ✓ PASS

技術用語の正確な使用

`rewhere`, `take!`, `CollectionProxy`, スコープ汚染など、Railsの文脈における技術用語が正確に使用されています。

説明の技術的正確性 ✓ PASS

技術的主張の正確性と論理性

`where`と`rewhere`の挙動の違いや、`find_by!`から`take!`への変更理由についての説明が技術的に正確かつ論理的です。

事実の突合 ✓ PASS

PR情報による主張の裏付け(ハルシネーション検出)

記事内の主張はすべてPRのDescriptionやDiff内のコード・テストで裏付けられており、ハルシネーションは検出されませんでした。

数値・固有名詞の確認 ✓ PASS

PR番号・コミットID・バージョン等の正確性

PR番号(#57223)や関連Issue番号(#57192)などの固有名詞が正確に記載されています。

タイトル・説明との一致 ✓ PASS

記事タイトル・説明とPR内容の一致

記事のタイトルはPRのタイトル("Fix duplicate `where` conditions in `create_or_find_by`")を正確に反映しており、内容とも一致しています。

外部知識の正確性 ✓ PASS

PRに記載のない外部知識(LTS、サポート状況など)の不使用

バージョン情報やリリース予定など、PR情報に記載のない外部知識の追加はありません。

時間表現の正確性 ✓ PASS

時間表現がPR情報と一致しているか

「〜でした」「〜が修正されました」といった過去形の表現が適切に使われており、PRの文脈と一致しています。