accepts_nested_attributes_for の :limit が単一ハッシュで誤計上するバグを修正
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 形態での動作が保証され、既存機能との互換性も維持されています。