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
end
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
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
.