Fix `create_or_find_by` scope pollution on the non-transactional retry

rails/rails

Rails 8.1で create_or_find_by 系メソッドが、トランザクション外で再試行した際にスコープが二重に適用されレコードが取得できなくなるバグを修正しました。これにより、トップレベルの呼び出しでも期待どおりに既存レコードを返すようになります。

背景

create_or_find_by がスコープ汚染を起こすケースは、ActiveRecord::RecordNotUnique が発生した後のリトライ処理に起因します。 #57192 でトランザクション内の if connection.transaction_open? ブランチは rewhere(attributes).lock.take! に置き換えられましたが、else ブランチは依然として find_by!(attributes) を呼び出していました。

else ブランチのコードはスコープを二度適用し、条件が矛盾して RecordNotFound を引き起こします。 例えば Subscriber.where(nick: "alice").create_or_find_by(nick: "bob")WHERE "nick" = 'alice' AND "nick" = 'bob' という不可能な条件を生成し、既に存在するレコードを返せませんでした。

テストがこのバグを捕捉できなかったのは、テストスイートが常にトランザクション内で実行されるため connection.transaction_open? が常に真になるからです。 その結果 else ブランチは実行されず、実運用でのみ顕在化していました。

技術的な変更

activerecord/lib/active_record/relation.rb の実装を二箇所修正し、else ブランチでも rewhere(attributes).take! を呼び出すように統一しました。 変更前後の差分は以下の通りです。

@@ -283,7 +283,7 @@ def create_or_find_by(attributes, &block)
         if connection.transaction_open?
           rewhere(attributes).lock.take!
         else
-          find_by!(attributes)
+          rewhere(attributes).take!
         end
       end
     end
@@ -303,7 +303,7 @@ def create_or_find_by!(attributes, &block)
         if connection.transaction_open?
           rewhere(attributes).lock.take!
         else
-          find_by!(attributes)
+          rewhere(attributes).take!
         end
       end
     end

.lock の付与はトランザクション内でのみ意味があるため、else ブランチでは除去しました。 rewhere(attributes).take! はスコープを再適用しつつロックなしで単一レコードを取得するので、汚染が防がれます。

テストコード activerecord/test/cases/create_or_find_by_outside_transaction_test.rb を新規追加し、トランザクション外での挙動を検証しました。 テストは self.use_transactional_tests = false に設定し、実際のトップレベル呼び出しと同様の環境を再現しています。

# frozen_string_literal: true

require "cases/helper"
require "models/subscriber"

class CreateOrFindByOutsideTransactionTest < ActiveRecord::TestCase
  self.use_transactional_tests = false

  def setup
    Subscriber.create!(nick: "cofb_bob", name: "Bob")
  end

  def teardown
    Subscriber.where(nick: "cofb_bob").delete_all
  end

  def test_create_or_find_by_with_polluted_scope_outside_a_transaction
    existing = Subscriber.find_by!(nick: "cofb_bob")
    found = Subscriber.where(nick: "cofb_alice").create_or_find_by(nick: "cofb_bob")
    assert_equal existing, found
  end

  def test_create_or_find_by_bang_with_polluted_scope_outside_a_transaction
    existing = Subscriber.find_by!(nick: "cofb_bob")
    found = Subscriber.where(nick: "cofb_alice").create_or_find_by!(nick: "cofb_bob")
    assert_equal existing, found
  end
end

設計判断

トランザクション有無に関係なく同一ロジックを適用する方針で実装を統一しました。 既存の if connection.transaction_open? 分岐は残しつつ、else 側でも同等の rewhere(...).take! を用いることで、コードパスの差異を最小化しました。

ロック取得 (.lock) を除去したのは、トランザクション外ではロックが作用せず無駄になるためです。 この選択は機能的な影響を与えず、パフォーマンスとシンプルさのトレードオフとして妥当です。

まとめ

今回の修正は、create_or_find_by 系メソッドがトランザクション外で再試行した際にスコープ汚染を起こすバグを除去し、トップレベル呼び出しでも正しいレコード取得を保証します。実装は既存ロジックを踏襲しつつ最小限の変更で済ませており、後方互換性を保ったまま信頼性を向上させました。

記事メタデータ

Generated by:
gpt-oss-120b for DiffDaily
LLM Trace:
a4591ede

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

品質レビュー結果

Review Status:
承認済み
Review Count:
1回
Reviewed by:
gpt-oss-120b for DiffDaily

Review Criteria:

記事構成 ✓ PASS

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

リード文、背景、技術的変更、設計判断、まとめが順序立てて配置されており、総論→各論→結論の構成が明確です。

カスタムMarkdown構文 ⚠ WARNING

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

コードブロックは正しい形式ですが、PRリンクが `[PR #57711](URL)` となっており、指定の `[#123](URL)` 形式になっていません。機能上の問題はありません。

対象読者への適合性 ✓ PASS

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

Rails の内部実装に精通したエンジニア向けの内容で、初心者向けの過剰な説明はありません。

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

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

各セクションは総論・各論・結論の段落構成が整っており、段落はトピックセンテンスで始まり、1段落1トピック・適切な長さが保たれています。

Diff内容との照合 ✓ PASS

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

記事内の diff と提供された diff が完全に一致しており、コード引用に誤りはありません。

技術用語の正確性 ✓ PASS

技術用語の正確な使用

使用されている用語は PR 内容と一致し、誤用は見られません。

説明の技術的正確性 ✓ PASS

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

変更理由や影響の説明が PR の記述と整合しており、技術的に正確です。

事実の突合 ✓ PASS

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

全ての主張が PR のタイトル・説明・Diff で裏付けられており、推測や捏造はありません。

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

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

PR 番号 #57711、関連 PR #57192 など数値は正確に記載されています。

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

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

記事タイトルは PR タイトルと一致しています。

外部知識の正確性 ✓ PASS

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

外部のバージョンサポート情報やリリース日程など、PR に無い情報は含まれていません。

時間表現の正確性 ✓ PASS

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

時間表現の歪曲はなく、PR の記述と一致しています。