How to Manage Null Constraints With Migrations in Ruby on Rails
Ruby on Rails makes it easy to write migrations to modify your database schema.
But null constraints are a topic that requires some special attention. If you don't handle null constraints properly, a poorly written migration can easily wreak havoc on your database and bring down your production server.
What is a Null Constraint?
By default most databases allow you to store
NULL values in your table columns. Sometimes you might want to specify that a certain value has to exist, and you can apply a null constraint to enforce that.
Then whenever a record attempts to save a
NULL value, the database transaction will roll back nicely and prevent it from doing so.
Adding a Column With a Null Constraint
When you write a migration, you can specify null constraints whenever you add new columns.
add_column helper, you can add the
null keyword argument to specify if the new column allows
NULL values or not. You have the option to add the
default keyword argument too if you need to define what the default value of this column should be. This will ensure that new records have a value even if you don't specify one, so they can abide by your null constraint.
Here's how this might look:
add_column :articles, :published, :boolean, null: false, default: false
This will add a new
boolean column called
published to the
articles table. The
null: false parameter means this column does not allow
NULL values. The
default: false tells us this column will default to false whenever a value isn't specified.
This is nice because we can guarantee this field will always be either true or false now. It can't be null because the database prevents it. I'd wager that you've probably caused or at least seen a bug from code that didn't handle null values correctly.
Preventing them at the database level can significantly simplify your logic and save you from future bug squashing!
Creating a Table With Columns That Have Null Constraints
If you are creating a new table, the syntax is similar.
We will use the
create_table helper to define our table and columns.
Then append the
null: false keyword argument to any column in your
create_table call. You can also add the
default: in the same way to define a default value on a column.
create_table "articles", force: true do |t| t.string "name", null: false t.text "body" t.boolean "published", default: false end
Changing a Null Constraint on an Existing Column
Removing null constraints on existing columns is no problem, but if we add a new null constraint we run into big problems.
The thing about migrations is they need to be reversible.
You want to be able to roll them back. For example, let's say you pushed a new release to your production environment and later discovered some critical bugs. You want to be able to roll back that release to the prior version where everything worked.
But if your migrations aren't reversible, you can't always do that.
If we write a migration to allow null values, add a few records with null values, and then try to roll back the migration... that migration will fail since the table has null values and it can't enforce the new null constraint.
How to Write Reversible Null Constraint Migrations
This is critically important, but also trivially easy most of the time.
When we need to add a null constraint on a column, we just have to make sure that the column has a default value that we can use to replace the current
In other words, if we specify that a column cannot have any null values, we have to replace every
NULL value with a not null value in order to apply that constraint.
We can use the
change_column_null helper to update the null constraint on an existing column. It takes a few arguments and should look something like this:
def change change_column_null(:articles, :name, false, "Untitled") end
Digging into the arguments:
- Table name -
- Column name -
- Null constraint, can this column be null?
- Default value, if this column no longer allows
NULL, what do we replace
This migration is reversible and will let you add or remove null constraints with no issues.
Other Ways to Write Reversible Migrations
I'll admit there is some Ruby magic happening in the example above.
In a migration when you define the
change method, Active Record will try its best to roll back the migration just by doing the opposite. In other words, it knows when you call
add_column to pretty much call
remove_column on the roll back. The same is true for the
There are cases where you can't write a reversible migration all in the
change method though. One example is if you need to do a data migration or backfill some data of some kind too.
In these scenarios, it's best to refer to the older style of writing migrations.
The Up and Down Methods
When you run a migration, say with
rake db:migrate or
rake db:migrate:rollback, under the hood your migration is either running the
You can also run
rake db:migrate:status to see which migrations are currently up or down (in other words which have run or not).
If you're writing a migration that isn't reversible, it's best to fall back to defining the specific
down methods instead of jamming everything into the
change method and relying on magic.
Here's a quick example of the
def up add_column :articles, :published, :boolean rename_column :articles, :featured, :temporary_featured end def down rename_column :articles, :temporary_featured, :featured remove_column :articles, :published end
The idea is we explicitly tell the database how to roll back our migration with the
down method. We have more control over what happens.
Thought most of the time
change is good enough!