マルチパラメータ属性代入で `nil` を `""` と同等に扱うよう修正
Active Recordのマルチパラメータ属性代入において、nil 値が渡された際に発生していた NoMethodError を修正しました。value.empty? を value.blank? に置き換えるだけの1行の変更で、nil と "" が意味的に等価に扱われるようになります。
背景
マルチパラメータ属性代入は、Date 型や DateTime 型のカラムに対して "last_read(1i)" のような分割されたキーでハッシュを渡すと、Active Recordが1つの値に合成する仕組みです。フォームから年・月・日を別々のフィールドで受け取るケースで利用されます。
これまで、各パラメータに "" を渡した場合は「値が不在」として nil に変換されていました。しかし、同じく「値が不在」を表す nil を渡すと、value.empty? が NilClass に対して呼ばれ NoMethodError が発生していました。フォームの実装やデータの変換処理によっては "" ではなく nil が渡されることがあり、予期しない例外の原因となっていました。
topic = Topic.where.not(last_read: nil).first
# "" の場合: 正常動作
topic.attributes = { "last_read(1i)" => "", "last_read(2i)" => "", "last_read(3i)" => "" }
topic.last_read # => nil
# nil の場合: NoMethodError が発生
topic.attributes = { "last_read(1i)" => nil, "last_read(2i)" => nil, "last_read(3i)" => nil }
# => NoMethodError: undefined method 'empty?' for nil
"" と nil はどちらも「値が存在しない」ことを示す点で意味的に同等であり、同じ結果(nil)を返すべき入力です。
技術的な変更
変更箇所は activerecord/lib/active_record/attribute_assignment.rb の1行のみです。extract_callstack_for_multiparameter_attributes メソッド内で value.empty? を value.blank? に置き換えました。
変更前:
parameter_value = value.empty? ? nil : type_cast_attribute_value(multiparameter_name, value)
変更後:
parameter_value = value.blank? ? nil : type_cast_attribute_value(multiparameter_name, value)
String#empty? は String にしか定義されておらず、nil に対して呼び出すと NoMethodError になります。一方、Object#blank? はActive Supportが NilClass を含むあらゆるオブジェクトに定義しており、nil.blank? は true を返します。String#blank? の内部では String#empty? が呼ばれるため、"" に対する既存の挙動は変わりません。
あわせて activerecord/test/cases/multiparameter_attributes_test.rb に test_multiparameter_attributes_on_date_with_all_nil テストが追加され、既存の test_multiparameter_attributes_on_date_with_all_empty と対になる形でリグレッションが防止されています。
設計判断
blank? への置き換えという最小限のアプローチ が採用されました。nil を事前に "" に変換するガード節を追加するなど他の修正方法も考えられますが、blank? は nil と "" の両方で true となるため、1文字の変更で両方のケースを一元的に処理できます。
Active Supportの blank? を利用することで、条件分岐を増やさずにRailsの既存のヘルパーメソッドに処理を委譲しています。Active Recordは既にActive Supportに依存しているため、外部依存が増えることもありません。blank? が nil / "" / 空白文字列のいずれにも true を返す点は、フォーム入力の正規化という文脈でも自然な選択です。
まとめ
value.empty? を value.blank? に変えるだけの1行修正ですが、nil と "" という意味的に等価な2つの入力を一貫して扱えるようにすることで、入力の正規化に関する暗黙の前提を取り除きました。Active Supportの blank? を活用することで、コードの変更量を最小限に抑えつつ、マルチパラメータ属性代入の堅牢性が向上しています。