image by Obi Onyeador
Always Force Booleans to be True or False
Migrations, in common with much of Rails, are ‘sharp knives’ — easy to use, but potentially dangerous, and you can end up cutting yourself.
One way a migration can ‘cut’ you is by letting you make quick decisions about your database structure without considering the consequences. In this case we’ll examine the case of NULL
in a boolean field your SQL. This is converted to nil
when you use it in Ruby.
Instead of…
…allowing boolean values to be NULL
in your database.
class AddRecommendedIndexes < ActiveRecord::Migration[5.1]
def change
add_column :characters, :satisfied, :boolean
end
end
Always…
…use constraints in your database that prevent values that should be only true
or false
from being NULL
.
class AddRecommendedIndexes < ActiveRecord::Migration[5.1]
def change
add_column :characters, :satisfied, :boolean, default: false, null: false
end
end
But why?
This is a known issue called the Tri-state problem. Although nil
behaves a lot like false
in Ruby, they are not quite the same. Having a third state really complicates the logic of your application as you have to consider how nil
should be treated.
It also complicates your scopes. If you think of nil
as false
you’d have to use scopes like:
Character.where(satisfied: false).or(Character.where(satisfied: nil))
Character.where.not(satisfied: true)
Which is much more confusing than:
Character.where(satisfied: false)
Why not?
If you’re starting from scratch, just do this.
If you already have this issue — and heaven knows I’ve fallen into the trap — you will have to put together a migration strategy.
Changing or adding columns or default values to an existing table will lock the table in many databases, while it writes to every row. This isn’t a problem unique to this issue, but it is a problem when ‘fixing’ it.
This means you’ll have to consider downtime or perhaps multiple smaller migrations to ensure you minimise the impact on your application.
Last updated on December 17th, 2018