Locks

Use Multiple Migrations When Adding Database Constraints

Adding constraints to your application at the database level is a good idea as it provides an extra layer of quality control on top of the data powering your application.

One way to do that is to add default values or constraints—like making sure a field can’t be blank. This is easy to do with Rails’ migrations. But beware…

Instead of…

…adding a column and stipulating a default non-null value all in one go:

class AddComposerToSongs < ActiveRecord::Migration[5.2]
  def change
    add_column :songs, :composer, :string, default: "Lin-Manuel Miranda", null: false
  end
end

Use…

…a ‘multiple migration’ strategy to add a field with a constraint to your databases.

Add a column

class AddComposerToSongs < ActiveRecord::Migration[5.2]
  def change
    add_column :songs, :composer, :string
  end
end

Deploy code that sets default, in your application

class Song < ApplicationRecord
  before_save :set_default_composer

  def set_default_composer
    self.composer = "Lin-Manuel Miranda" if composer.nil?
  end
end

This will catch any code that writes to the database during this intermediate stage.

Update records with nil values

Using a rake task or using rails console.

Song.where(composer: nil).find_each do |song|
  song.update(composer: "Lin-Manuel Miranda")
end

Change the column to have a constraint

class AddRecommendedIndexes < ActiveRecord::Migration[5.1]
  def change
    change_column :songs, :composer, :string, default: "Lin-Manuel Miranda", null: false
  end
end

…and then you can delete the Ruby code that sets a default.

But why?

If you take the “all in one” approach you run the risk of significant downtime for your application.

If you set null: false in your migration, the database will rewrite the whole table, locking it whilst doing so. This may take a significant amount of time, depending on how much data you already have stored. Locking your database’s table will likely cause write timeouts for any users trying to write to your database at the same time.

The multi-stage deployment is a bit of a pain, but it enables you to keep the application available for your users during any migration of data tables.

Why not?

If you’re starting a new app or have a very limited dataset.

You might get away with a whole table rewrite and write-lock if you have low amounts of traffic writing to the database. But you would be getting away with it. Chances are you’re going to have some downtime.

If you don’t mind putting your application into maintenance mode, where users are blocked from using the site, you could run the migration with downtime.


originally posted on 13 Jan 2019 by @andycroll

photo by
Marcos Mayer

Don’t miss my next post, sign up to the One Ruby Thing email and get my next post in your inbox.

Don’t miss my next post, sign up to the One Ruby Thing email and get my next post in your inbox.