Most often, ActiveRecord migrations are used to manage database schema changes. Any migration you write should meet one, steadfast rule:

The code in a migration should be able to run, in sequence, at anytime.

Sometimes, however, the data in the database needs to change as a result of a migration. It is tempting to use models inside a migration to affect the data change. This can be done, but you should consider that when external code (your models) is used in a migration, it ties the migration to code that may and most likely will change over time; violating the above rule.

The Problem

If you work on a code base long enough (especially as part of a team), you'll inevitably experience pain when "the Rule" is violated. Consider this example, where Maria, Tim, and Jamie are working on the same code base which contains an App model:

Maria creates a migration for the apps table which adds and populates a new app_code column. She also adds a validation to the App model for the app_code column.

# db/migrate/20130815231023_add_app_code_to_app.rb
class AddAppCodeToApp < ActiveRecord::Migration
  def up
    add_column :apps, :app_code, :string
    App.reset_column_information
    App.find_each do |app|
      app.update_attribute :short_code, App.generate_code
    end
  end

  def down
    remove_column :apps, :app_code
  end
end

# app/model/app.rb
class App < ActiveRecord::Base
  # ...
  validates :short_code, presence: true
  # ...
  def self.generate_code
    char_set = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
    (1..10).map { char_set[rand(62),1] }.join()
  end
end

Tim pulls Maria's changes and runs the migrations with rake db:migrate.

Tim then creates a new migration which adds another column to the apps table and also adds a validation to the App model.

# db/migrate/20130819170554_add_branding_to_app.rb
class AddBrandingToApp < ActiveRecord::Migration
  def up
    add_column :apps, :branding, :boolean
    App.reset_column_information
    App.find_each do |app|
      app.update_attribute :branding, true
    end
  end

  def down
    remove_column :apps, :branding
  end
end

# app/model/app.rb
class App < ActiveRecord::Base
  # ...
  validates :branding, :short_code, presence: true
  # ...
end

All is well until Jamie returns from vacation, pulls the latest changes from Maria and Tim, and runs the outstanding migrations with rake db:migrate.

Jamie's migration crashes when the model attempts to save because it tries to validate the presence of :branding, a column which is not in the database when the first migration runs.

rake aborted!
An error has occurred, this and all later migrations canceled:

undefined method `branding' for #<Product:0x000001049b14a0>

Had Maria and Tim observed "the Rule", this pain point could have been avoided. In such cases, the goal is to rely on NO external code in your migration. All code that's needed, including your model, should be defined in the migration itself.

The Solution

Now, let's see the code where Maria and Tim observed "the Rule."

Maria's migration would look like:

# db/migrate/20130815231023_add_app_code_to_app.rb
class AddAppCodeToApp < ActiveRecord::Migration

  class App < ActiveRecord::Base

    def self.generate_code
      char_set = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
      (1..10).map { char_set[rand(62),1] }.join()
    end
  end

  def up
    add_column :apps, :app_code, :string
    App.reset_column_information
    App.find_each do |app|
      app.update_attribute :short_code, App.generate_code
    end
  end

  def down
    remove_column :apps, :app_code
  end
end

Because Maria cannot rely on the definition of the class method generate_code to be the same or even be present in the App model when the migration is run, she duplicates it inside the migration. In this case, it’s not a violation of DRY, but rather a code revision freeze, as the migration should always run the same way.

Similarly, Tim's migration would look like:

# db/migrate/20130819170554_add_branding_to_app.rb
class AddBrandingToApp < ActiveRecord::Migration
  class App < ActiveRecord::Base
    # Intentionally empty
  end

  def up
    add_column :apps, :branding, :boolean
    App.reset_column_information
    App.find_each do |app|
      app.update_attribute :branding, true
    end
  end

  def down
    remove_column :apps, :branding
  end
end

Tim adds an empty App model within the migration to keep Rails from running validations or other callbacks which may prevent it from completing.

Now, when Jamie returns from her relaxing vacation, she is able to update her code base, run the outstanding migrations, and start to work on the next awesome feature.

Summary

To keep developers and deploys from experiencing painful migration problems, faithfully ensure your migrations meet the following criteria:

The code in a migration should be able to run, in sequence, at anytime.