Rails

Rails – Healthy Migration Habbits – a repost

We are using a gem that encrypts database table columns. So far, it is working as intended, however, we encountered several issues where a column or a model attribute is missing, or simply not working. It turns out that we do migrations wrong.

Database Migration

Database migration is one of the features of any web application framework that allows developers to alter tables or data in a programmable and consistent manner. In Ruby on Rails, we sometimes write migrations that not only migrate table definitions, but also migrate data.

Data Migration

With data migration, it is common to use the model directly. However, the model and the database schema, most of the times, can be out of sync since the model is always the latest version and the schema may be a few migrations way from the latest. The result is that the migration will fail.

According to this guide – Healthy Migration Habbits – we should never reference the model directly.

Adjustments

Here is our model where we are trying to apply ssn with encryption. The attr_encrypted gem requires a column called encrypted_orig_column_name, in this case, the encrypted_ssn.

class User < ActiveRecord::Base
  attr_encrypted :ssn, encryptor: CustomEncryptor
end

Usually, we will write this migration file:

class UserEncryption < ActiveRecord::Migration[5.0]
  def up
    add_column :users, :encrypted_ssn, :text
    User.reset_column_information
    User.all.each do |node|
      node.update!(encrypted_ssn: User.encrypt_ssn(node.ssn))
    end
    remove_column :users, :ssn
  end

  def down
    add_column :users, :ssn, string, limit: 191
    User.reset_column_information
    User.all.each do |node|
      node.update!(ssn: User.decrypt_ssn(node.encrypted_ssn))
    end
    remove_column :users, :encrypted_ssn
  end
end

However, this might not work at all as our original ssn column is not yet encrypted, but the model already assumes it is encrypted. The proposed method is to create a bare ActiveRecord class so that it is free from interference from the original model.

class UserEncryption < ActiveRecord::Migration[5.0]
  class MigrateUser < ActiveRecord::Base
    self.table_name = :users
  end

  def up
    add_column :users, :encrypted_ssn, :text
    User.reset_column_information
    MigrateUser.all.each do |node|
      node.update!(encrypted_ssn: User.encrypt_ssn(node.ssn))
    end
    remove_column :users, :ssn
  end

  def down
    add_column :users, :ssn, string, limit: 191
    User.reset_column_information
    MigrateUser.all.each do |node|
      node.update!(ssn: User.decrypt_ssn(node.encrypted_ssn))
    end
    remove_column :users, :encrypted_ssn
  end
end

We will need the model functionality, ie: the ability to encrypt/decrypt. What we did is to access the column directly using the bare active record class so that attr_encrypted won’t get in the way.

See Healthy Migration Habits for more tips.

Leave a reply

Your email address will not be published. Required fields are marked *