`pluck`でstrict_loading違反が検出されなかったバグを修正

rails/rails

CollectionProxy#pluckを呼び出した際にstrict_loading違反が検出されず、N+1クエリがサイレントに実行されていた問題が修正されました。合わせて、violates_strict_loading?がプリロード済みのアソシエーションでも誤って違反と判定していた問題も解消されています。

背景

strict_loading!を設定したリレーションでpluckを呼び出しても、StrictLoadingViolationErrorが発生しないというバグが#51524として報告されていました。to_aload経由のアクセスでは正しくエラーが発生するにもかかわらず、pluckだけが例外を素通りさせていたのです。

問題を再現するコードは明快です。Post.all.strict_loading!で取得したレコードに対し、posts.first.comments.to_aStrictLoadingViolationErrorを発生させます。一方、posts.first.comments.pluck(:id)は同じN+1クエリを発行するにもかかわらず、エラーも警告も発生させずにクエリを実行していました。

このバグが発生していた理由は、CollectionProxy#pluckexec_queriesload_targetを経由せず直接クエリを実行する独自のコードパスを持っており、そこにstrict_loadingのチェックが組み込まれていなかったためです。

技術的な変更

今回の修正は2つの独立した変更から構成されています。CollectionProxy#pluckへのチェック追加と、violates_strict_loading?のアクセス修飾子変更です。

activerecord/lib/active_record/associations/collection_proxy.rbpluckメソッドに、違反チェックのガード節が追加されました。

変更前:

def pluck(*column_names)
  null_scope? ? scope.pluck(*column_names) : super
end

変更後:

def pluck(*column_names)
  if proxy_association.violates_strict_loading?
    Base.strict_loading_violation!(owner: proxy_association.owner.class, reflection: proxy_association.reflection)
  end

  null_scope? ? scope.pluck(*column_names) : super
end

これにより、クエリ実行前にviolates_strict_loading?を評価し、違反があればstrict_loading_violation!を呼び出してエラーまたはログ出力を行います。

もう一つの変更は、violates_strict_loading?メソッドのアクセス修飾子の変更です。このメソッドはactiverecord/lib/active_record/associations/association.rbprivateセクションに定義されていましたが、CollectionProxy#pluckから直接呼び出す必要が生じたため、publicメソッドに昇格されました。合わせて、メソッドの先頭にreturn unless find_target?という条件が追加されています。

変更前:

# private セクション内
def violates_strict_loading?
  return if @skip_strict_loading

  return unless owner.validation_context.nil?

  return reflection.strict_loading? if reflection.options.key?(:strict_loading)

  owner.strict_loading? && !owner.strict_loading_n_plus_one_only?
end

変更後:

# public セクション内
def violates_strict_loading?
  return unless find_target?

  return if @skip_strict_loading

  return unless owner.validation_context.nil?

  return reflection.strict_loading? if reflection.options.key?(:strict_loading)

  owner.strict_loading? && !owner.strict_loading_n_plus_one_only?
end

追加されたreturn unless find_target?は、プリロード済みのアソシエーションに対してviolates_strict_loading?trueを返してしまう問題を防ぐためのものです。find_target?はアソシエーションのターゲットがまだロードされていない場合にtrueを返すメソッドであり、既にプリロードされている場合はfalseを返します。これによりDeveloper.preload(:audit_logs).first.audit_logs.pluck(:id)のような正当な使い方が誤ってエラーを発生させることがなくなります。

設計判断

violates_strict_loading?をpublicに昇格させてCollectionProxyから直接呼び出す設計が採用されています。

代替案として、exec_queriesや別の共通パスにチェックを集約する方法も考えられますが、pluckexec_queriesを経由しないコードパスを持つため、そのアプローチは取られませんでした。関連する#50389がクエリメソッド全般における同種の問題をexec_queriesへのチェック追加で対処しているのと対照的に、本PRはpluck固有のコードパスに対してピンポイントの修正を施しています。

find_target?によるガードは、プリロードとstrict_loadingを組み合わせた際の正当なユースケースを壊さないための重要な配慮です。strict_loadingの目的はN+1クエリの検出であり、プリロード済みのデータへのアクセスはその定義からして問題ではありません。このロジックをviolates_strict_loading?自体に組み込むことで、今後同メソッドを呼び出す箇所が増えた場合でも一貫した判定が保証されます。

まとめ

この修正により、pluckを使用した際のstrict_loading違反が他のアクセスパターンと同様に検出されるようになり、N+1クエリ検出の一貫性が確保されました。violates_strict_loading?へのfind_target?ガードの追加は、プリロード済みアソシエーションへの正当なアクセスを保護しつつ、strict_loadingの本来の目的を正確に実装し直したものといえます。

記事メタデータ

Generated by:
Claude Sonnet 4.6 for DiffDaily
LLM Trace:
2fae5129

この記事は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のIssue/PRへのリンク記法([#123](URL))が正しく使用されています。

対象読者への適合性 ✓ PASS

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

内容はRailsの内部実装に関するものであり、専門知識を持つエンジニアという対象読者に完全に適合しています。

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

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

各セクション、各パラグラフが「総論→各論」の構成で書かれています。トピックセンテンスが明確で、1段落1トピックが守られており、非常に読みやすいです。

Diff内容との照合 ✓ PASS

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

記事内で引用されているコードブロック(変更前・変更後)は、提供されたDiff情報と完全に一致しています。ファイルパスの指定も正確です。

技術用語の正確性 ✓ PASS

技術用語の正確な使用

「CollectionProxy」「strict_loading」「proxy_association」など、Railsの技術用語が正確かつ適切に使用されています。

説明の技術的正確性 ✓ PASS

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

「`pluck`が独自のコードパスを持つ」「`find_target?`がプリロード済みかを判定する」といった技術的な説明は、Diffの内容と整合しており、正確かつ論理的です。

事実の突合 ✓ PASS

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

記事の主張はすべてPRのTitle、Description、Diffのコードから裏付けられています。特に、関連PR(#50389)との比較はPR Descriptionの情報を基にしており、ハルシネーションは見られません。

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

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

PR番号(#51627)、Issue番号(#51524)、関連PR番号(#50389)など、すべての数値・固有名詞が正確に記載されています。

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

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

記事タイトル「`pluck`でstrict_loading違反が検出されなかったバグを修正」は、PRの主題を正確に反映しています。

外部知識の正確性 ✓ PASS

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

PRで言及されていない外部知識(バージョンのサポート状況、リリース日程など)の捏造は見られません。

時間表現の正確性 ✓ PASS

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

「修正されました」「発生していた」などの時間表現は、PRの文脈と一致しており、歪曲はありません。