`through` アソシエーションの `unscope` がジョインモデルのデフォルトスコープを無視するバグを修正

rails/rails

has_many :through アソシエーションで unscope を使用した際、ジョインモデルのデフォルトスコープが誤って解除されてしまうバグが修正されました。unscope_values にテーブル名を付与することで、スコープ解除の対象を正確に限定します。

背景

has_many :through アソシエーションに unscope を含むラムダスコープを定義すると、対象のアソシエーションだけでなくジョインモデルのデフォルトスコープまで意図せず解除されるという問題がありました(#48548)。

問題の発生条件は以下のような構成です。Post モデルが default_scope { where(deleted_at: nil) } を持ち、Comment モデルも同様のデフォルトスコープを持つ場合に、has_many :comments, -> { unscope(where: :deleted_at) } を定義すると、User から through: :posts でコメントを取得する際に、posts テーブルの deleted_at スコープも解除されてしまいます。結果として、論理削除済みの投稿に紐づくコメントまで取得対象に含まれてしまいます。

unscope の値として渡される :deleted_at はテーブル名を持たないシンボルであるため、チェーンされたクエリのどのテーブルに対する条件なのかを区別できないことが根本原因です。

技術的な変更

unscope_values を適用する際にテーブル名で修飾するメソッドを追加し、アソシエーションスコープの結合処理でこれを使用するよう変更されました。

association_scope.rb の変更: アソシエーションチェーンを結合する add_constraints メソッド内で、unscope_values をそのまま適用していた箇所が新しいメソッドを使うよう変更されました。

変更前:

scope.unscope!(*item.unscope_values)

変更後:

scope.unscope!(*item.table_name_qualified_unscope_values)

query_methods.rb への追加: Relation クラスに table_name_qualified_unscope_values メソッドと、それが内部で使う qualify_attribute_with_table_name メソッドが追加されました。

def table_name_qualified_unscope_values
  self.unscope_values.map do |scope|
    case scope
    when Hash
      scope.transform_values do |target_value|
        case target_value
        when Array
          target_value.map { |value| qualify_attribute_with_table_name(value) }
        when Symbol, String
          qualify_attribute_with_table_name(target_value)
        else
          target_value
        end
      end
    else
      scope
    end
  end
end

def qualify_attribute_with_table_name(attr)
  attr.to_s.include?(".") ? attr : predicate_builder.resolve_arel_attribute(table_name, attr)
end

qualify_attribute_with_table_name は、属性名にすでにドット(.)が含まれている場合はそのまま返し、含まれていない場合は predicate_builder.resolve_arel_attribute を使って table_name.attr_name 形式の Arel 属性に変換します。これにより、unscope(where: :deleted_at)comments.deleted_at として解釈され、posts.deleted_at のスコープには影響しなくなります。

unscope_values の構造は [{ where: :deleted_at }] のようなHashの配列であるため、変換処理はHashのvalueに対して行われます。valueが Array の場合は各要素を個別に修飾し、Symbol / String の場合は直接修飾することで、unscope(where: [:deleted_at, :published_at]) のような複数属性の指定にも対応しています。

設計判断

unscope_values を変換するメソッドを Relation 側に追加するというアプローチが採られました。

代替案として、unscope! の呼び出し前に add_constraints 側で変換処理を行うことも考えられますが、今回は Relation クラスのメソッドとして実装されています。これにより、変換ロジックがテーブル名の解決手段(predicate_builder)と同じスコープに置かれ、table_name への参照も自然な形で行えます。

また、すでにテーブル名で修飾された属性(ドットを含む文字列)はそのまま通すガードが設けられており、二重修飾を防いでいます。既存の unscope_values の構造を変更せず、読み取り時にのみ変換する設計であるため、unscope! 自体の動作には影響しません。

まとめ

本修正は、unscope_values を適用する時点でテーブル名を付与するという一点の変更によって、has_many :through チェーン上のスコープ解除が正確なテーブルに限定されるようになります。テーブル名を持たないシンボルがアンビギュアスに解釈されていた問題を、Arel の属性解決機構を活用することで根本から解消した修正です。

記事メタデータ

Generated by:
Claude Sonnet 4.6 for DiffDaily
LLM Trace:
745c5cdc

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

品質レビュー結果

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

Review Criteria:

記事構成 ✓ PASS

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

総論→各論→結論の構成が明確です。リード文、背景、技術的な変更、設計判断、まとめの各セクションがすべて含まれており、それぞれの役割を適切に果たしています。

カスタムMarkdown構文 ✓ PASS

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

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

対象読者への適合性 ✓ PASS

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

Active Recordのアソシエーションやスコープに関する深い内容であり、専門知識を持つエンジニアという対象読者に完全に適合しています。

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

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

各セクション内が総論→各論で構成され、各段落はトピックセンテンスで始まっています。1段落1トピックの原則が守られており、可読性が非常に高いです。

Diff内容との照合 ✓ PASS

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

記事で引用されているコードブロックは、提供されたDiffの内容と完全に一致しています。ファイル名も正確です。

技術用語の正確性 ✓ PASS

技術用語の正確な使用

`unscope_values`, `predicate_builder`, `Arel` などの技術用語が正確かつ適切な文脈で使用されています。

説明の技術的正確性 ✓ PASS

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

新しく追加された `table_name_qualified_unscope_values` メソッドの役割や、それによってバグがどのように解消されるかについての説明は、技術的に正確で論理的です。

事実の突合 ✓ PASS

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

記事内の主張はすべてPR情報(Description、Diff)で裏付けられており、ハルシネーションは見られません。「設計判断」セクションもコードの構造から読み取れる事実に基づいており、適切な解説です。

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

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

PR番号(#48549)およびIssue番号(#48548)が正確に記載されています。

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

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

記事のタイトルはPRが解決する問題を具体的に示しており、PRの内容と完全に一致しています。

外部知識の正確性 ✓ PASS

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

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

時間表現の正確性 ✓ PASS

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

「修正されました」という過去形の表現は、マージ済みのPRを解説する記事として適切であり、時間表現に問題はありません。