`reset_counters` が文字列IDを受け取ると0にリセットされるバグを修正

rails/rails

reset_counters に文字列型のIDを渡すと、カウンターが正しい値ではなく0にリセットされるバグが修正されました。主キーの型キャストを適切に行うことで、文字列IDでも整数IDと同様に正確なカウントが設定されるようになります。

背景

reset_counters は、counter_cacheの値をDBの実際の関連レコード数に合わせて再計算・更新するメソッドです。しかし #56122 および #57185 で報告されたように、@topic.id.to_s のように文字列型のIDを渡すと、カウンターが正しい値ではなく0にリセットされる問題がありました。

この問題は、reset_counters 内部でIDをクエリ条件に使用する際、渡された文字列IDが主キーの型(整数)に一致しないため、一致するレコードが見つからず結果として0が設定される挙動から生じていました。Topic.reset_counters(@topic.to_param, "replies") のように、ルーティングから取得したパラメータ(文字列)をそのまま渡すユースケースで再現します。

技術的な変更

修正の核心は、IDリストを構築した後に type_for_attribute で取得した主キーの型情報を使い、各IDを適切な型にキャストする処理の追加です。

変更前:

def reset_counters(id, *counters, touch: nil)
  ids = if composite_primary_key?
    if id.first.is_a?(Array)
      id
    else
      [id]
    end
  else
    Array(id)
  end

変更後:

def reset_counters(id, *counters, touch: nil)
  if composite_primary_key?
    ids = id.first.is_a?(Array) ? id : [id]
    types = primary_key.map { |column| type_for_attribute(column) }
    ids = ids.map do |id_value|
      id_value.map.with_index { |id, i| types[i].cast(id) }
    end
  else
    ids = Array(id)
    type = type_for_attribute(primary_key)
    ids = ids.map { |id| type.cast(id) }
  end

通常の主キーの場合、Array(id) でIDの配列を作成した後、type_for_attribute(primary_key) で主キーの型オブジェクトを取得し、type.cast(id) で各IDを適切な型に変換します。複合主キー(CPK) の場合は、各カラムに対応する型オブジェクトを配列で取得し、map.with_index を使ってそれぞれのカラムの型でキャストします。

テストは2つ追加されています。通常の主キーモデル向けに Topic.reset_counters(@topic.id.to_s, :replies) が正しく動作することを検証するテスト、およびCPKモデル向けに Cpk::Order.reset_counters(string_id, :books) を検証するテストです。後者のテストのためにフィクスチャ (cpk_orders.yml, cpk_books.yml) も追加されています。

設計判断

IDの型キャストをメソッド入口で一元化するアプローチが採用されました。

type_for_attribute はActiveRecordが既存のスキーマ情報から型オブジェクトを取得するAPIであり、外部ライブラリへの依存なくモデルの主キー型を正確に取得できます。cast はActiveRecordの型システムが提供する変換メソッドであり、"1"1 に変換するといった標準的な型強制を担います。これにより、文字列・整数どちらで渡されても後続の処理が型一致した値を受け取ることが保証されます。

また、この変更はCPKと非CPKの両ブランチで対称的に型キャストを適用しており、将来的な主キー型の変更に対しても一貫した動作を提供します。

まとめ

本PRは、reset_counters の引数として渡されるIDを主キーの型情報でキャストするという、局所的かつ低リスクな修正によって問題を解消しました。to_param やURLパラメータ経由で文字列IDが渡される実際のユースケースに対応しており、通常の主キーと複合主キーの両方で安全に動作するようになっています。

記事メタデータ

Generated by:
Claude Sonnet 4.6 for DiffDaily
LLM Trace:
bc9b3537

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

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

対象読者への適合性 ✓ PASS

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

内容は`reset_counters`や複合主キー(CPK)など、Railsの内部実装に関するもので、専門知識を持つエンジニアという対象読者に完全に適合しています。

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

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

各セクションが総論→各論で構成され、各段落はトピックセンテンスで始まり、1段落1トピックが守られています。段落の長さも適切で、非常に読みやすい構成です。

Diff内容との照合 ✓ PASS

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

記事内のコードブロックは、提供されたDiff情報を正確に反映しています。変更前後のコード引用、ファイル名ともに一致しており、テストコードに関する説明もDiffの内容と整合性が取れています。

技術用語の正確性 ✓ PASS

技術用語の正確な使用

`type_for_attribute`、`cast`、`複合主キー(CPK)`といった技術用語が、文脈に沿って正確に使用されています。

説明の技術的正確性 ✓ PASS

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

「文字列IDが型キャストされずにクエリに使われるためレコードが見つからない」というバグの原因や、「`type.cast`で型を強制する」という修正内容の説明は、技術的に正確かつ論理的です。

事実の突合 ✓ PASS

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

記事内のすべての主張(関連Issue番号、バグの挙動、修正アプローチなど)は、PRのTitle、Description、Diff内容によって裏付けられており、ハルシネーションは検出されませんでした。

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

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

PR番号(#57187)およびIssue番号(#56122, #57185)が正確に記載されています。

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

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

記事のタイトル「`reset_counters` が文字列IDを受け取ると0にリセットされるバグを修正」は、PRのタイトルと内容を的確に要約しており、主題のズレはありません。

外部知識の正確性 ✓ PASS

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

記事に含まれる情報はすべてPRおよび関連情報から導出可能であり、サポート状況やリリース予定といったPR外の知識の捏造は見られませんでした。

時間表現の正確性 ✓ PASS

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

「修正されました」「問題がありました」など、完了した変更に対する過去形の表現が適切に使用されており、時間表現の歪曲はありません。