複合外部キーを持つ `belongs_to touch: true` が旧レコードをタッチしない問題を修正

rails/rails

複合外部キーを持つ belongs_to アソシエーションで touch: true を設定している場合、外部キーが変更されたとき旧レコードの updated_at が更新されないサイレントバグを修正しました。touch_record メソッドが複合キーの変更検出に対応したことで、単一・複合いずれの外部キーでも正しく動作するようになります。

背景

touch: true 付きの belongs_to は、レコードの保存・削除時に親レコードの updated_at を更新する仕組みです。しかし複合外部キーの場合、外部キーを変更しても 親レコードのタッチが発生しないバグが存在していました。

問題の根本原因は touch_record メソッド内の changes[foreign_key] というハッシュルックアップにありました。saved_changes"shop_id""order_id" のように個別のカラム名をキーとして持つのに対し、複合外部キーは ["shop_id", "order_id"] という配列として渡されていたため、ルックアップは常に nil を返していました。結果として old_foreign_id が取得できず、旧親レコードへのタッチ処理がサイレントにスキップされていました。

例えば、BookOrder 1 から Order 2 へ移動した場合、Order 2updated_at は更新されますが、Order 1 はタッチされないまま残るという問題です。

技術的な変更

touch_recordold_foreign_id 取得ロジックが複合キー対応に拡張されました。変更の核心は、外部キーが配列かどうかを判定し、各カラムを個別に changes ハッシュで検索する点にあります。

変更前:

def self.touch_record(o, changes, foreign_key, name, touch) # :nodoc:
  old_foreign_id = changes[foreign_key] && changes[foreign_key].first

変更後:

def self.touch_record(o, changes, foreign_key, name, touch) # :nodoc:
  old_foreign_id =
    if foreign_key.is_a?(Array)
      if foreign_key.any? { |fk| changes[fk] }
        foreign_key.map do |fk|
          changes[fk] ? changes[fk].first : o.read_attribute(fk)
        end
      end
    elsif changes[foreign_key]
      changes[foreign_key].first
    end

複合外部キーの場合、foreign_key.any? { |fk| changes[fk] } でいずれかのカラムが変更されたかを確認します。変更があれば、各カラムについて「changes[fk] に変更前の値があればそれを、なければ read_attribute で現在値を」取得することで、旧複合キーの値を完全に復元します。これにより、複合キーの一部のみが変更されたケースも正確に処理されます。

find_by の呼び出しにも小さな修正が加わっています。

変更前:

old_record = klass.find_by(primary_key => old_foreign_id)

変更後:

old_record = klass.find_by(primary_key => [old_foreign_id])

[old_foreign_id] と配列でラップすることで、単一主キー・複合主キーのどちらでも Array(primary_key).zip(...) 的なマッピングが一貫して動作するようにしています。

テスト面では activerecord/test/models/cpk/book.rbbelongs_to :ordertouch: true が追加され、cpk_orders テーブルに t.timestamps カラムが追加されました。テストケースとして以下の3シナリオが timestamp_test.rb に追加されています:

  • 通常の保存時に CPK 親レコードがタッチされること
  • 削除時に CPK 親レコードがタッチされること
  • 複合キーの一部変更時に新旧両方の親レコードがタッチされること

設計判断

単一外部キーと複合外部キーを 分岐で処理する方式 が採用されました。既存の単一外部キーのコードパスは elsif ブランチとして残され、動作変更がありません。

複合キーの旧値復元において、o.read_attribute(fk) へのフォールバックが重要な役割を果たしています。複合キーを構成する全カラムが変更されるとは限らないため、変更されていないカラムは現在のレコードの値をそのまま旧キーの一部として使います。これにより「一部のみ変更された複合キー」というエッジケースを追加のデータ取得なしに処理しています。

find_by への [old_foreign_id] ラップは、複合主キーに対して find_by を呼ぶ際の内部的なキーマッピングの一貫性を保つための変更です。複合外部キーの場合 old_foreign_id はすでに配列になっているため、単一キーとの扱いを揃える意図があります。

まとめ

この修正は、touch_record における外部キーの変更検出を単一キーと複合キーの両方に対して正しく機能させるものです。changes ハッシュのルックアップを個別カラム単位に改め、旧複合キーの組み立てロジックを追加することで、既存の単一外部キーのコードパスに影響を与えずにサイレントバグを解消しています。

記事メタデータ

Generated by:
Claude Sonnet 4.6 for DiffDaily
LLM Trace:
6ad82e9f

この記事は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リンク記法の正確性

ファイル名付きシンタックスハイライト(```言語:ファイルパス)や、PRへのリンク記法が正しく使用されています。

対象読者への適合性 ✓ PASS

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

複合外部キーや `touch` オプションといったRailsの内部実装に関する専門的な内容を、対象読者であるエンジニア向けに過不足なく解説しています。

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

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

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

Diff内容との照合 ✓ PASS

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

記事で引用されているコード(変更前・変更後)は、提供されたDiffの内容と完全に一致しており、正確です。

技術用語の正確性 ✓ PASS

技術用語の正確な使用

`changes` ハッシュ、`read_attribute`、複合外部キーといった技術用語を文脈に沿って正確に使用しています。

説明の技術的正確性 ✓ PASS

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

バグの原因(配列キーによるハッシュルックアップの失敗)から修正ロジック(個別カラムのチェックと旧キーの再構築)まで、技術的な説明は正確かつ論理的です。

事実の突合 ✓ PASS

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

記事内のすべての主張は、PRのDescriptionやDiffで裏付けられています。「設計判断」セクションもコードの意図を正確に解説しており、ハルシネーション(創作)は見られません。

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

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

PR番号(#57158)やファイルパスなどの固有名詞はすべて正確に記載されています。

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

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

記事のタイトルはPRの主題「Fix touch_record for composite foreign keys」を、より具体的に分かりやすく要約しており、内容と完全に一致しています。

外部知識の正確性 ✓ PASS

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

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

時間表現の正確性 ✓ PASS

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

時間表現に関する問題はありません。「旧レコード」「変更前」といった表現はPRの文脈に沿っており正確です。