accepts_nested_attributes_for の :limit が単一ハッシュで誤計上するバグを修正

rails/rails

Rails の accepts_nested_attributes_for:limit オプションで許容レコード数を制御できますが、単一レコードを示すハッシュが複数属性を持つと誤って TooManyRecords が発生していました。この変更はその根本原因を除去し、期待通りのバリデーションを実現します。

背景

accepts_nested_attributes_for:limit はコレクション属性の更新件数を上限としてチェックしますが、id キーだけを持つハッシュが内部で配列に正規化される前にレコード数が判定されていました。その結果、ハッシュのキー数(例: id, name, color, breed の4つ)をレコード数とみなして TooManyRecords がスローされました。

根本原因は check_record_limit! が正規化処理より前に実行されていた点にあります。このメソッドは渡されたコレクションの size をそのままレコード数と解釈するため、単一レコードハッシュが多数の属性キーを含むと誤計上していました。ハッシュ‑オブ‑ハッシュ形式 ({"0"=>..., "1"=>...}) では Hash#size が実際のレコード数と一致していたため、問題が顕在化しませんでした。

技術的な変更

check_record_limit! の呼び出し位置を正規化後に移動することで、レコード数が正しくカウントされるようにしました。具体的には assign_nested_attributes_for_collection_association メソッド内で、ハッシュを配列へラップするロジックの直後に check_record_limit! を配置し直しています。

@@
-        check_record_limit!(options[:limit], attributes_collection)
-
         if attributes_collection.is_a? Hash
           keys = attributes_collection.keys
           attributes_collection = if keys.include?("id") || keys.include?(:id)
@@
-        end
-
-        association = association(association_name)
+        end
+
+        check_record_limit!(options[:limit], attributes_collection)
+
+        association = association(association_name)

テストスイートに新規テストを追加し、すべての :limit 設定形式で期待通りに動作することを検証しましたNestedAttributesLimitTests モジュールに test_limit_does_not_count_the_attributes_of_a_single_record_hash を実装し、limit: 2 の状態で 4 キーのハッシュを渡しても例外が出ないことを確認しています。

+  def test_limit_does_not_count_the_attributes_of_a_single_record_hash
+    parrot = @pirate.parrots.create!(name: "Polly")
+
+    @pirate.attributes = { parrots_attributes: { "id" => parrot.id, "name" => "New Name", "color" => "green", "breed" => 1 } }
+    assert_equal "New Name", parrot.name
+  end

設計判断

check_record_limit! の位置変更は既存ロジックへの最小侵入で実現されています。正規化後にレコード数を評価するだけで、他のコードパスやオプション形式(数値、シンボル、Proc)に影響を与えません。これにより、既存の :limit 機能はそのまま保持されつつ、誤検出のみが解消されました。

後方互換性は完全に保たれます。ハッシュが配列に変換された後にチェックするという動作は、従来の配列入力やハッシュ‑オブ‑ハッシュ入力に対しても同等の制限判定を行うため、既存のアプリケーションが期待した例外を受け取るケースは変わりません。唯一の変更は単一レコードハッシュが正しく処理されるようになる点です。

まとめ

この PR は accepts_nested_attributes_for:limit バリデーションが単一レコードハッシュを誤ってカウントしていたバグを修正し、正規化後にレコード数を評価するように設計を見直しました。テスト追加によりすべての :limit 形態での動作が保証され、既存機能との互換性も維持されています。

記事メタデータ

Generated by:
gpt-oss-120b for DiffDaily
LLM Trace:
6ee22be1

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

コードブロックは ````ruby:ファイルパス```` 形式で正しく記述され、GitHubリンクも #57560 がリンク化されています。形式的な誤りはありません。

対象読者への適合性 ✓ PASS

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

Rails の内部実装やテストコードに関する記述が中心で、専門エンジニア向けの内容です。初心者向けの過度な説明はありません。

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

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

各セクションは総論パラグラフで要点を示し、続く段落で具体例・コードを展開、最後に結論パラグラフでまとめています。トピックセンテンスが先頭にあり、1段落1トピック・6文以内で構成されています。

Diff内容との照合 ✓ PASS

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

記事中のコードブロックは提供された Diff と完全に一致しています。削除・追加位置・内容ともに差分通りです。

技術用語の正確性 ✓ PASS

技術用語の正確な使用

`check_record_limit!`、`TooManyRecords`、`accepts_nested_attributes_for` などの用語が PR と一致し、誤用は見られません。

説明の技術的正確性 ✓ PASS

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

根本原因・修正内容・影響についての説明が PR の記述と整合しており、技術的に正確です。

事実の突合 ✓ PASS

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

全ての主張が PR のタイトル、説明、Diff で裏付けられています。外部情報や根拠のない数値は提示されていません。

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

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

PR 番号 #57560、ファイルパス、テストコードの行数など、数値・固有名詞は正確です。

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

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

記事タイトルは PR の内容「:limit が単一ハッシュで誤計上するバグを修正」と一致しています。

外部知識の正確性 ✓ PASS

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

記事内に PR には記載されていないバージョンサポート情報やリリース日程等の外部知識は含まれていません。

時間表現の正確性 ✓ PASS

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

時間表現の歪曲はなく、PR の記述と一致しています。