I discovered a fun quirk of Active Record today. I’m not sure if it’s a bug or if it’s just me doing something silly, but in these kinds of cases I’ve learned to lean more towards the latter. Here’s how it’s done.
Start with a fresh Rails app, using MySQL:
rails new shop -d mysql
Create a new Variant
model:
rails g model variant price:decimal
We just so happen to know that we need to specify a precision (numbers before the decimal) and scale (numbers after the decimal) for this column, and we (read: I) can never remember the syntax for that in the generate
command, so we edit the migration to provide both a precision
and scale
for this column, turning it into this:
class CreateVariants < ActiveRecord::Migration
def change
create_table :variants do |t|
t.decimal :price, precision: 10, scale: 2
t.timestamps null: false
end
end
end
Running this migration with rake db:migrate
will generate a db/schema.rb
which looks like this:
ActiveRecord::Schema.define(version: 20151110060233) do
create_table "variants", force: :cascade do |t|
t.decimal "price", precision: 10, scale: 2
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
end
A show columns from variants;
MySQL query will show this:
+------------+---------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+------------+---------------+------+-----+---------+----------------+
| id | int(11) | NO | PRI | NULL | auto_increment |
| price | decimal(10,2) | YES | | NULL | |
| created_at | datetime | NO | | NULL | |
| updated_at | datetime | NO | | NULL | |
+------------+---------------+------+-----+---------+----------------+
Note here that the price
field has a precision
of 10 and scale
of 2; that’s indicated by decimal(10,2)
. That’s good, because that’s what we specified inside our migration.
Let’s stop here and create three variants in our console:
rails console
Variant.create(price: 18.49)
Variant.create(price: 26.99)
Variant.create(price: null)
These are just for demonstration purposes. No real data was harmed in the writing of this blog post.
The price
column here doesn’t have a default value and it allows for null values. There’s data in here already which has null
and that just won’t do, because we want prices to always be a decimal number to make it easy to manage in our code. Having to do to_f
to convert nil
to 0.0
is not that smart. We’re better than that!
So we’ll go ahead and create a new migration to do the the default
and null
modifications to this price
column:
rails g migration add_default_to_variants_price
Inside that migration, we write this:
class AddDefaultToVariantsPrice < ActiveRecord::Migration
def change
Variant.where(price: nil).update_all("price = 0")
change_column :variants, :price, :decimal, default: 0.0, null: false
end
end
Pretty simple stuff. We change all the variants where the price is nil
(null
) to have a price set to 0
. Then we set the default
to be 0.0
and also tell it that we don’t want null
values in this column. If we didn’t do the update_all
call first, AR would bomb out because we tried to set null: false
on a column which already had nulls.
We then run this migration (rake db:migrate
), and this is where things go bad.
To start with, let’s open rails console
and run Variant.first
. This is the variant that has a price set to 26.99
. Well, it’s not that any more:
#<Variant id: 1, price: 27,
Yes, it’s now 27.
What happened? Let’s look at our schema.rb
:
ActiveRecord::Schema.define(version: 20151110061535) do
create_table "variants", force: :cascade do |t|
t.decimal "price", precision: 10, default: 0, null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
end
Ok, that says it’s got a precision: 10
, but where’s the scale from the first migration gone? It’s gone away.
Let’s confirm this in MySQL too:
mysql> SHOW COLUMNS from variants;
+------------+---------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+------------+---------------+------+-----+---------+----------------+
| id | int(11) | NO | PRI | NULL | auto_increment |
| price | decimal(10,0) | NO | | 0 | |
| created_at | datetime | NO | | NULL | |
| updated_at | datetime | NO | | NULL | |
+------------+---------------+------+-----+---------+----------------+
The precision has stayed at 10, but the scale has changed to 0.
But why does this happen?
Because in the second migration, we didn’t specify a precision and a scale.
Instead of this line:
change_column :variants, :price, :decimal, default: 0.0, null: false
We should have:
change_column :variants, :price, :decimal, precision: 10, scale: 2, default: 0.0, null: false
Otherwise, it defaults to a precision of 10 and scale of 0 and helpfully rounds up your prices for you.
Isn’t that handy?