Using Models In Rails Migrations
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 => 'email@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.
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 => 'firstname.lastname@example.org', :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.