Using Models In Rails Migrations

by serge on May 19th, 2010

Every once in a while we may want to do something like this:

 
class CreateUsers < ActiveRecord::Migration
  def self.up
    create_table :users do |t|
      t.string :name, :null => false
      t.string :email, :null => false
      t.string :hashed_password, :null => false
      t.string :salt, :null => false
      t.timestamps
    end
 
    add_index :users, :email, :unique => true
 
    User.create!(:name => 'Default User',
      :email => 'user@example.com', :password => 'demo',
      :password_confirmation => 'demo')
  end
 
  def self.down
    drop_table :users
  end
end

Does it look good? If the answer is yes, then the rest of this small post worth reading.

Once upon a time

Let’s pay closer attention to the line where we’re creating default user record. The fact that it works and does its job well doesn’t mean that we’re safe. And that is due to the fact that we’re referencing a piece of application code from the database migration script. Migration files and application code have different life cycles. While application code evolves, migration code, once committed to the master branch (i.e. shared with the rest of the team/world), should stay the same forever (well, we’re doing grown up development, right?). We need to be extra careful when referencing any application code from migrations. For example, months after that migration was successfully committed and pushed, User model changes:

 
# new migration gets added...
 
class AddUsersBirthday < ActiveRecord::Migration
  def self.up
    add_column, :users, :birthday, :date, :null => false
  end
 
  def self.down
    remove_column, :users, :birthday
  end
end
 
# and User model gets a new validation:
 
class User < ActiveRecord::Base
 
  # ...
 
  validates_presence_of :birthday
 
  # ...
 
end

Now, if we try to recreate database from scratch, old migration which creates users table will not work for obvious reasons. Better yet, things could get even more interesting as User could have been renamed to Admin at this point.

Special cases

Sometimes using models for data migration is tempting. SQL often gets not as pretty as we would like to; seldom we just can’t do data conversions in SQL at all (consider hashing operations on passwords as an example). But in all such cases validations, relationships and algorithms that we may want to utilize in migrations can easily change as application develops.

One obvious solution for this problem is not to use models in migrations at all. Just DDL, plain SQL updates with execute is not such a bad idea actually. Another way is to declare migration local models like this:

 
class CreateUsers < ActiveRecord::Migration
 
  class User < ActiveRecord::Base
    include Behaviors::HashedPassword
  end
 
  def self.up
    User.reset_column_information
    create_table :users do |t|
      # ...
    end
 
    User.create!(:name => 'Default User',
      :email => 'user@example.com', :password => 'demo')
  end
 
  def self.down
    drop_table :users
  end
end

And if data conversion requires some piece of model implementation, it’s better to copy that code into migration local model class. In such cases it’s not a violation of DRY. It’s rather a code revision freeze, as migration should always run the same way.

Just don’t do it

If all migration data manipulation gravitates around bootstrapping your database with some starter content (like the above example with creating a user instance), then it may better fit into a dedicated rake task. Migrations should deal with schema changes, not data population. And often having to deal with static table content (which is often used as options for various selects, status id/name dictionary for example) can be a sign of more general design problem of putting code constants into db.

From rails

3 Comments
  1. Dermot permalink

    Initial data population for an app can be done with db/seed.rb

  2. data population SHOULD be done with seed.rb