JT Hopple LLC

Reliable Migrations

JeremyProgramming, Rails, Ruby Digg!

Ruby on Rails migrations rule! They're easy to get started with and have changed the way I approach database-driven application development. However, I was recently shocked when I used capistrano to deploy a new version of an application and one of the migrations exploded. It turns out that using ActiveRecords in your migrations can lead to unexpected results, but by following two simple guidelines you can ensure that your migrations will remain reliable throughout your application's life-cycle.

Migrations are primarily used for two things: 1) to alter the database schema, and 2) to move data around. Using ActiveRecord models in migrations is an incredibly powerful and productive way to make data adjustments after having altered the schema, but this practice can backfire.

The following guidelines will ensure that migrations remain reliable even when they use ActiveRecord models:

  1. Always reset the column information for an ActiveRecord after having altered it's table.
  2. If you plan to use ActiveRecords in your migration, define them locally in the migration.

In one of our applications, we were convinced that we needed a "sophisticated" role-based user authentication system, but after getting real with the application we quickly realized that a much simpler user system would work. So, we wrote the following migration to remove the more complicated role-based system in favor of a simple admin boolean in the users table:

  1. class simplify_users < ActiveRecord::Migration
  2. def self.up
  3. add_column :users, :admin, :boolean, :default => false
  4. User.find(:all).each do |user|
  5. user.update_attribute(:admin, true) if user.roles.include?(Role.find_by_name("admin"))
  6. end
  7. drop_table :roles
  8. drop_table :user_roles
  9. end
  10. def self.down
  11. create_table :roles do |t|
  12. t.column :name, :string
  13. end
  14. create table :user_roles do |t|
  15. t.column :user_id, :integer, :null => false
  16. t.column :role_id, :integer, :null => false
  17. end
  18. remove_column :users, :admin
  19. end
  20. end

There are two problems with this migration. The first problem is that we've added a new column to the users table and then we attempt to use the User model to update this attribute. If this migration is run by itself, everything will run fine as the column information for the ActiveRecord, User in this case, is loaded the first time it is referenced. However, if this migration isn't run by itself and the User model was referenced in a previous migration we'll get an error, "NoMethodError: undefined method 'admin' for User."

Fixing this problem is easy and illustrates our first guideline. Always reset the column information for an ActiveRecord after having altered it's table (line 7):

  1. class simplify_users < ActiveRecord::Migration
  2. def self.up
  3. add_column :users, :admin, :boolean, :default => false
  4. #since we altered the users table, we need to reset the column
  5. #information prior to using the User model
  6. User.reset_column_information
  7. User.find(:all).each do |user|
  8. user.update_attribute(:admin, true) if user.roles.include?(Role.find_by_name("admin"))
  9. end
  10. drop_table :roles
  11. drop_table :user_roles
  12. end
  13. def self.down
  14. create_table :roles do |t|
  15. t.column :name, :string
  16. end
  17. create table :user_roles do |t|
  18. t.column :user_id, :integer, :null => false
  19. t.column :role_id, :integer, :null => false
  20. end
  21. remove_column :users, :admin
  22. end
  23. end

The second problem has to do with the fact that we're assuming that the ActiveRecord models we're using in our migration actually exist. In the case of our example migration we dropped the roles and user_roles tables and removed the User and UserRole models from our application (and our svn repository). Since these models no longer existed, our migration failed with "NameError: uninitialized constant UserType." This leads us to our second guideline. If you plan to use ActiveRecords in your migration, define them locally in the migration (lines 4-13):

  1. class simplify_users < ActiveRecord::Migration
  2. #since we need to use both the User and Role models in this migration,
  3. #let's define them here along with the relationships we'll need to use.
  4. class User < ActiveRecord::Base
  5. has_many :roles, :through => :user_roles
  6. end
  7. class UserRole < ActiveRecord::Base
  8. belongs_to :user
  9. belongs_to :role
  10. end
  11. class Role < ActiveRecord::Base; end
  12. def self.up
  13. add_column :users, :admin, :boolean, :default => false
  14. #since we altered the users table, we need to reset the column
  15. #information prior to using the User model
  16. User.reset_column_information
  17. User.find(:all).each do |user|
  18. user.update_attribute(:admin, true) if user.roles.include?(Role.find_by_name("admin"))
  19. end
  20. drop_table :roles
  21. drop_table :user_roles
  22. end
  23. def self.down
  24. create_table :roles do |t|
  25. t.column :name, :string
  26. end
  27. create table :user_roles do |t|
  28. t.column :user_id, :integer, :null => false
  29. t.column :role_id, :integer, :null => false
  30. end
  31. remove_column :users, :admin
  32. end
  33. end

By defining the ActiveRecords locally in the migration, you can be certain that the migration will run successfully regardless of whether the model still exists in the application. Also, as you may have noticed by the fact that the User has_many roles, it is necessary that you specify any relationships that you plan to use in your migration in your local ActiveRecord definitions. This may seem like a hassle, but, in addition to running into problems if you try to use an ActiveRecord that doesn't exist anymore, you can also run into problems by using relationships, methods, or attributes that don't exist in the ActiveRecord anymore.

The bottom line is that migrations are meant to run on any version of your database and upgrade it to the current version (or whichever version you specify). You just have to keep in mind that the source of your application could be drastically different at the time you run your migrations than when you created them. Following the guidelines outlined here will help you ensure that your migrations remain reliable.

References: Screencast: Migrating data and schema , The Joy of Migrations , Safely using models in migrations

0 Comments

Post a comment