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