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.
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
endAssuming 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
endNow, 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
endYou 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
endIn 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
endThen, 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
endThis might be also useful if you want to write an extension for versioned_fields.