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.