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.