一言でまとめると、integerなどの数値系のカラムに空文字を代入しないこと。
以下、平成も終わるというのに未だにRails4.1で疲弊している男のつぶやきです。
こんな感じの商品テーブルがあるとします。
create_table :products do |t|
t.string :name, null: false, default: ''
t.integer :price, null: false, default: 0
t.timestamps null: false
end
default: 0
が設定されているのでnewするとpriceは0です。
product = Product.new
product.price # => 0
ここで空文字を代入してみます.
product = Product.new
product.price = ''
product.price # => nil
ふむふむ、空文字はnilとして評価されて、デフォルト値の0から変更されている(ように見えます)。
null: false
なのでこのままでは保存できないのかなと思いきや...
product.price # => nil
product.changes #=> {} ???
product.save #=> true ????
product.price # => 0 ?????
一瞬priceがnilになっていたのに、changesが更新されておらず、priceはデフォルトの0のままで保存されるというのが、今回紹介したい謎挙動です。
Rails4.2にアップデートした時に、このバグのような挙動を前提にしていたコードが動かなくなってハマりました。
Rails4.2以降だと、普通にchangesが更新されます。
product = Product.new
product.price = ''
product.price # => nil ここまでは同じ
product.changes # => {"price"=>[0, nil]} ちゃんと0→nilに更新されている
product.save # => null: falseなのでちゃんとraiseする
空文字以外の文字列はto_i
された結果が入るので問題無いようでした。
product = Product.new
product.price = '1'
product.price # => 1
product.changes # => {"price"=>[0, 1]}
integerだけでなく、decimalやfloatでも同様の挙動を示します。
今回はwrite_attribute
をoverrideして、数値系カラムに空文字を代入している箇所をログ出力し、コードを修正してから再度アップデートにチャレンジすることになりました。
class ActiveRecord::Base
def write_attribute(attr_name, value)
column = self.class.columns.find { |c| c.name.to_s == attr_name.to_s }
if column && column.type.in?(%i[integer decimal float]) && value == ''
Rails.logger.warn "Deprecated: assigning blank string into #{column.type} column. #{caller.first}"
end
super
end
end
callerメソッドって初めて使いましたが便利ですね。
5.2への道のりは長い...