Versioned Fields

VersionedFields gem was created to help you gracefully modify data in you columns without downtime. It allows you to migrate fields only when the appropriate record is accessed.

Usage

Before the start

To make the gem work you need to create a column called yourfield_version. For example, if you have a foo column, you need to create a column called foo_version.

Propagate it with default value, lets say - 1. Example of Rails migration:

class CreateFooVersion < ActiveRecord::Migration[5.2]
  def change
    add_column :users, :foo_version, :integer, default: 1, null: false
  end
end

Using VersionedFields migrations

Assuming you have users table with field address and address_version.

On version 1 (initial version) we have addresses like Green Avenue, #1516.

On version 2 we want to append city to it: Los Angeles, Green Avenue, #1516.

Let’s write a migration. Create db/migrate_versioned_fields/user/address.rb file, and put there the following code:

# db/migrate_versioned_fields/user/address.rb

VersionedFields::Migration.draw_for(User, :address) do
  version 1 # No block, since it's a first version
  version 2 do
    # Write here code needed to migrate from v1 to v2
    # You can use model methods here
    "Los Angeles #{address}"
  end
end

Now, restart your server. When you access a certain user (i.e. - User.find(22)), it’s address value will be automatically upgraded to version 2, with the city name.

Note that writing to the DataBase will be performed before you’ll receive your model object. So in the end you’ll receive user with a correct address, migrated to the latest version.

At the same time, until you did not access that particular user, its address will be in the state of outdated version. So searching through the database will be still considering outdated version!

Now let’s assume you decided to replace city with state. Open migration file for address field, and add a new migration:

# db/migrate_versioned_fields/user/address.rb

VersionedFields::Migration.draw_for(User, :address) do
  version 1
  version(2)do
    "Los Angeles #{prev_value}"
  end

+ version(3)do
+   address.gsub('Los Angeles', 'LA')
+ end
end

You may write a background job which will migrate users slowly while server is running. It is as simple as:

# lib/tasks/migrate_with_zero_downtime.rake

namespace :users do
  task zero_downtime_migration: :environment do
    # Load every user one by one and update address
    User.find_each
  end
end

Separating concerns

In case your data migrations are huge, you may want to move them into separate module.

Lets say, you’ve decided to replace address with array of coordinates.

# lib/address_to_coordinates.rb

module AddressToCoordinates
  def address_to_coordinates
    GeocodingService.lookup(address).to_yaml
  end
end

Then, you can obviously include that module directly into User model definition, but since this module is only needed for migration purposes, it’s a better idea to have it right there:

# db/migrate_versioned_fields/user/address.rb

VersionedFields::Migration.draw_for(User, :address) do
  config.include AddressToCoordinates

  version 1
  version(2)do
    "Los Angeles #{prev_value}"
  end

  version(3) do
    address_to_coordinates
  end
end

This might be also useful if you want to write an extension for versioned_fields.