Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

invalid decimal becomes 0.0 in rails

I have the following rails model:

class Product < ActiveRecord::Base
end

class CreateProducts < ActiveRecord::Migration
  def self.up
    create_table :products do |t|
      t.decimal :price

      t.timestamps
    end
  end

  def self.down
    drop_table :products
  end
end

But when I do the following in the rails console:

ruby-1.9.2-p180 :001 > product = Product.new
 => #<Product id: nil, price: nil, created_at: nil, updated_at: nil> 
ruby-1.9.2-p180 :002 > product.price = 'a'
 => "a" 
ruby-1.9.2-p180 :003 > product.save
 => true 
ruby-1.9.2-p180 :004 > p product
#<Product id: 2, price: #<BigDecimal:39959f0,'0.0',9(9)>, created_at: "2011-05-18 02:48:10", updated_at: "2011-05-18 02:48:10">
 => #<Product id: 2, price: #<BigDecimal:3994ca8,'0.0',9(9)>, created_at: "2011-05-18 02:48:10", updated_at: "2011-05-18 02:48:10"> 

As you can see, I wrote 'a' and it saved 0.0 in the database. Why is that? This is particularly annoying because it bypasses my validations e.g.:

class Product < ActiveRecord::Base
  validates :price, :format => /\d\.\d/
end
like image 919
Thiago Avatar asked Feb 24 '23 04:02

Thiago


2 Answers

anything that is invalid gets cast to 0.0 if you call to_f on it

"a".to_f #=> 0.0

you would need to check it with validations in the model

validates_numericality_of :price # at least in rails 2 i think

i dont know what validating by format does, so i cant help you there, but try to validate that it is a number, RegExs are only checked against strings, so if the database is a number field it might be messing up

:format is for stuff like email addresses, logins, names, etc to check for illegeal characters and such

like image 140
loosecannon Avatar answered Feb 26 '23 22:02

loosecannon


You need to re-look at what is your real issue is. It is a feature of Rails that a string is auto-magically converted into either the appropriate decimal value or into 0.0 otherwise.

What's happening

1) You can store anything into an ActiveRecord field. It is then converted into the appropriate type for database.

>> product.price = "a"
=> "a"
>> product.price 
=> #<BigDecimal:b63f3188,'0.0',4(4)>
>> product.price.to_s
=> "0.0"

2) You should use the correct validation to make sure that only valid data is stored. Is there anything wrong with storing the value 0? If not, then you don't need a validation.

3) You don't have to validate that a number will be stored in the database. Since you declared the db field to be a decimal field, it will ONLY hold decimals (or null if you let the field have null values).

4) Your validation was a string-oriented validation. So the validation regexp changed the 0.0 BigDecimal into "0.0" and it passed your validation. Why do you think that your validation was bypassed?

5) Why, exactly, are you worried about other programmers storing strings into your price field?

Are you trying to avoid products being set to zero price by mistake? There are a couple of ways around that. You could check the value as it comes in (before it is converted to a decimal) to see if its format is right. See AR Section "Overwriting default accessors"

But I think that would be messy and error prone. You'd have to set the record's Error obj from a Setter, or use a flag. And simple class checking wouldn't work, remember that form data always comes in as a string.

Recommended Instead, make the user confirm that they meant to set the price to 0 for the product by using an additional AR-only field (a field that is not stored in the dbms).

Eg

attr_accessor :confirm_zero_price

# Validate that when the record is created, the price
# is either > 0 or (price is <= 0 && confirm_zero_price)
validates_numericality_of :price, :greater_than => 0, 
  :unless => Proc.new { |s| s.confirm_zero_price},
  :on => :create

Notes The above is the sort of thing that is VERY important to include in your tests.

Also I've had similar situations in the past. As a result of my experiences, I now record, in the database, the name of the person who said that the value should indeed be $0 (or negative) and let them have a 255 char reason field for their justification. Saves a lot of time later on when people are wondering what was the reason.

like image 45
Larry K Avatar answered Feb 26 '23 21:02

Larry K