A common change to make to a Rails application is to extract an attribute from a model into a one-to-many relationship. This change can be made without causing a large amount of downtime, even if there a significant amount of records needing to be changed.

In this post, I will describe how to change a model to replace an attribute with a one-to-many relationship while minimising downtime and emphasising continuous deployment.

The problem

We have a Person model with a name attribute and have been asked to change it to allow for a Person to have multiple Aliases, including their name. There are about a million people in the database, and minimising the downtime of the system is a high priority.

The naive solution is to change the system in a single deployment with a migration to create Alias and remove name. Such a deployment would take a long time, and could be very dangerous as ensuring such a large change to the system doesn’t break anything can be difficult.

A safer solution is to break down the change into multiple steps and alter the system over many deployments. Below I tried to break down the steps I have used in the past to solve such problems.

Step 1: Before Validate, Create

The first step is to create the Alias migration and model:

class CreateAliases < ActiveRecord::Migration

def change

create_table :aliases do |t|

t.integer :name

t.integer :person_id

end

end

end class Alias < ActiveRecord::Base

attr_accessible :name

belongs_to :person

end

Then edit the Person model to create an Alias without “hooking it up” to the main functionality:

class Person < ActiveRecord::Base

attr_accessible :name

has_many :aliases, :autosave => true

before_validation :upsert_alias def upsert_alias

alias = aliases.first || aliases.build

alias.name = self.name

end

...

This will allow the system to create all the Aliases in the background without causing an outage with:

Person.find_each do |person|

person.save

end

Step 2: Delegate and Drop

After step 1 we know that every person has exactly one Alias because any new Person is created with an Alias and all existing people have had an Alias attached.

Delegating the name functions from Person to the Alias will allow the new model to start being used while also having most of the old code keep working.

class Person < ActiveRecord::Base

attr_accessible :name

has_many :aliases, :autosave => true delegate :name, "name=", "name_changed?", to: :first_alias def first_alias

aliases.first || aliases.build

end

...

Once delegation is working name can be removed from Person. This may cause some pain as any code like Person.where(:name => 'bob') will break. It may also cause problems for other entities in your organisation (e.g. Data Warehouse) which may depend on database structure.

Step 3: Explicit Build

Altering the first_alias function to not build an alias if one doesn't exist, e.g.

def first_alias

aliases.first

end

will require functionality that creates people to explicitly create aliases. The previous assumption that when a Person's name is accessed an Alias will be created. Now the alias will have to be created explicitly for person.name to not break. Basically, anywhere a Person is created an alias must be added.

Step 4: Remove Delegation

The final stage is to remove the delegation from the Person model, i.e.

class Person < ActiveRecord::Base

has_many :aliases, :autosave => true

...

This is the most painful step, but can be accomplished slowly. As you maintain the code, or write new functions just ensure to not use person.name but person.firt_alias.name (or however you want to access the model).

Once the delegation has been removed you are done.

Conclusion

These steps may not work for your problem, as every application is unique in its own way (to paraphrase Tolstoy). However, when I come across similar problems I inevitably use a solution like the one described above as it gives me a lot of room to safely and slowly move my applications structure to how I need it.

References

Ruby Rogues episode Extreme Deployment has a great discussion about similar problems.

Continuous Delivery: Reliable Software Releases through Build, Test, and Deployment Automation