Reload時のスコープリークを修正 – all_queries デフォルトスコープの正しい適用

rails/rails

Reload時に all_queries デフォルトスコープが意図せず現在のスコープを混入し、余計な条件がクエリに付与されていた問題を解消します。all から default_scoped への切り替えで、スコープの境界が明確になり、期待通りのSQLが生成されます。

背景

reload が内部で self.class.all(all_queries: ...) を呼び出す設計のため、current_scope に含まれる非 all_queries スコープがリロードクエリに流出していました。問題は PR #57123 で導入された耐久性のある名前付きデフォルトスコープと併用した際に顕在化し、DeveloperWithDefaultMentorScopeAllQueries.reload が余計な salary 条件を付加していました。

このバグは「all が current_scope を包含する」ことが前提となっていた実装に起因し、all_queries だけを対象にした再読み込みが不可能になるという実害をもたらしていました。修正は all の呼び出しを排除し、default_scoped に委譲することで根本的にスコープの合成ロジックを分離します。

技術的な変更

activerecord/lib/active_record/persistence.rb_find_record メソッドで、all から default_scoped への置換が行われました。これにより all_queries: true が指定された場合は self.class.default_scoped(all_queries: true) が使用され、そうでない場合は従来通り self.class.all が呼び出されます。

@@
-        base = self.class.all(all_queries: all_queries).preload(strict_loaded_associations)
+        base = if all_queries
+          self.class.default_scoped(all_queries: true)
+        else
+          self.class.all
+        end
+
+        if all_queries && (current_scope = self.class.global_current_scope)
+          base = base.merge!(current_scope)
+        end
+
+        base = base.preload(strict_loaded_associations)

all_queries が有効なときは global_current_scope を明示的に取得し、merge! によって現在のグローバルスコープだけを追加します。これにより all が暗黙的に current_scope を巻き込むことはなくなり、all_queries が要求する純粋なデフォルトスコープだけが適用されます。

テストコードも同様に拡張され、test_default_scope_with_all_queries_does_not_apply_current_scope_on_reloadtest_default_scope_with_all_queries_applies_all_queries_current_scope_on_reload の2ケースで期待通りの SQL が生成されることを検証しています。これらのテストは scoping ヘルパーで all_queries フラグを操作し、リロード時のクエリ構造を直接比較しています。

以上の変更は既存の reload 挙動に最小限の侵入で済むよう設計されており、all_queries を使用しないコードパスは従来通り self.class.all が呼び出され続けます。

設計判断

default_scoped を利用して all_queries を処理する方針は、スコープ生成ロジックを一元化し可読性を向上させる意図があります。all が内部で current_scope を自動的にマージする複雑さを排除し、default_scoped が明示的に all_queries オプションを受け取る形に統一しました。

この選択は後方互換性を保つ点でも評価されます。self.class.all を呼び出す既存コードは変更されず、all_queries が真の場合だけ新しい経路に分岐するため、既存アプリケーションへの影響はなく安全にデプロイできます。さらに、global_current_scope の取得とマージは条件付きで実行されるため、不要な結合が発生しない点も設計上のメリットです。

結果として、スコープの境界が明確化されたことで、デフォルトスコープとリロードロジックの相互作用が予測可能になり、バグ再発防止に寄与します。

まとめ

reloadall_queries デフォルトスコープを正しく扱わなかったバグは、self.class.all から self.class.default_scoped への置換と、global_current_scope の条件付きマージにより解消されました。変更は既存挙動を壊さず、テストでカバーされた新しいスコープ適用ルールが導入されたことで、Rails のクエリ生成がより堅牢になりました。

記事メタデータ

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

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

品質レビュー結果

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

Review Criteria:

記事構成 ✓ PASS

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

リード文、背景、技術的変更、設計判断(任意)、まとめの5要素がすべて揃っており、総論→各論→結論の流れが明確です。まとめはリード文の単なる繰り返しではなく、変更の意義を再提示しています。

カスタムMarkdown構文 ✓ PASS

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

コードブロックは正しい diff 記法で記載され、ファイル名付きハイライトは使用していませんが問題ありません。PRリンクは「#57594」の形でハイパーリンク化されており、GitHubリンク記法も遵守されています。

対象読者への適合性 ✓ PASS

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

Rails の内部実装や ActiveRecord のスコープ機構に関する専門的な記述が中心で、エンジニア向けに適切なレベルです。初心者向けの過剰な解説はありません。

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

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

各セクションは総論・各論・結論の段落構成になっており、各段落はトピックセンテンスで始まり 1 段落 1 トピックが保たれています。段落は 5 文以内で、空行で区切られています。

Diff内容との照合 ✓ PASS

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

記事中の diff ブロックは PR で提供された diff と内容が一致しています。削除・追加行ともに正確に反映され、ファイル名や行番号の記載は省略されていますが情報としては問題ありません。

技術用語の正確性 ✓ PASS

技術用語の正確な使用

default_scoped、all_queries、global_current_scope などの用語は PR と一致しており、誤用は見られません。

説明の技術的正確性 ✓ PASS

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

変更の目的、実装手順、影響範囲について PR の記述と整合性が取れており、技術的に誤った説明はありません。

事実の突合 ✓ PASS

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

記事の全ての主張は PR の Title、Description、Diff で裏付けられており、推測や捏造はありません。

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

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

PR 番号 #57594 が正しく記載され、他の数値・固有名詞の誤りはありません。

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

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

記事タイトルは PR タイトルの意味を日本語で的確に表現しており、内容との不整合はありません。

外部知識の正確性 ✓ PASS

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

Rails のバージョンサポート状況やリリース日程など、PR に記載されていない外部知識は一切含まれていません。

時間表現の正確性 ✓ PASS

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

時間に関する表現はなく、PR の記述と矛盾する表現もありません。