Rails migrations are nice, but remembering the whole DSL can be hard.

Checklists are used by airplane pilots and doctors to avoid fatal errors with great success.

Forgetting to add an index to a column in your migration is hardly a fatal error, but why shouldn’t we use the same method to remember important steps in software development tasks?

Use the following checklist when writing Rails migrations to make sure you don’t forget something important.

If you are adding database-specific features that won’t get captured in the Ruby schema format (e.g. database views, triggers, stored procedures), edit your config/application.rb file and switch your config.active_record.schema_format setting from :ruby to :sql so that your schema dumps will include the features you’ve added in plain SQL.

Do you need to change the schema format to SQL?

# GOOD - reveals intent that migration should not be reversible

If your migration cannot be reversible, use the up / down methods (instead of change ) and raise a ActiveRecord::IrreversibleMigration error inside the down method to reveal your intent for it to be irreversible:

You should be able to run it up ( rake db:migrate ) and then down ( rake db:rollback ) without getting any errors and without changing the schema dump.

There is a lot of important stuff there so you should probably read it first if you haven’t.

Have you read the Rails guide on migrations ?

# BAD - the actual precision and scale this will use will be different depending on your database

Did you specify precision and scale when adding decimal columns?

# GOOD (if you are going to look up users by email)

# BAD - unlikely to look up users by password digest so this just takes up unnecessary space

# BAD (if you are going to look up users by email)

Add an index to columns that you know you will use to query by, but conservatively since they take up space and make inserts/updates slower:

# GOOD (if name is not required)

Instead, use a null constraint and if the field is not required, set the default to an empty string:

Avoid having both empty strings and null values in string columns since they don’t always get put in the same position when ordering.

Did you add a string column without specifying a default/null constraint?

# GOOD - fails validation before Rails tries to do the insert

Did you add a corresponding ActiveRecord validation for any database constraints so you get nicer validation errors?

Did you add unique constraints where you need to guarantee uniqueness?

Add a database constraint to be sure that bad data never makes it into your tables:

ActiveRecord presence validations won’t stop someone from inserting null values via update_attribute or raw SQL.

Use add_reference (or the alias add_belongs_to ) instead of add_column for a shorter, more intention-revealing syntax:

# GOOD - this will change all of the null values to "blue" and then add the constraint

# BAD - this will fail if the column already has null values

Use change_column_null to set the null constraint and replace all the null values in one go:

Are you adding a null: false option to a column that already has null values?

# BAD - this will fail if you try to run `rake db:rollback` after running `rake db:migrate`

Did you provide the optional arguments to make remove_column reversible?

Should you really be changing data inside your migrations? Some people are vehemently opposed to doing any kind of data manipulation inside their Rails migrations as they consider them only for changing the database schema. You should talk to the people you’re working with about how to handle data migrations and be aware of the pitfalls (see below) that can occur if you decide to do them inside your migrations. One alternative is to write separate rake tasks that perform the data migrations alone so that you can then run them at a known point and review the changes to make sure they worked. However you decide to handle data migrations, consider writing them in a way that minimizes the chance of losing data as much as possible. For example, if you’re splitting out a column into two new columns, first write a migration to add the new columns (and maybe also populate them or do this in a separate rake task). Then, only after you’re confident the data has been moved over successfully, write a migration to remove the old column. Ideally, each migration should still be fully reversible without losing data.

Are you iterating over a big table with each ? Calling .all.each on a model will try to instantiate all the objects at once, which can be really slow if you have lots of them. Instead, use find_each to find them in batches of 1000. Be aware that find_each will ignore any limits or orders you pass in (since it orders by id and limits by the batch size to find them in batches). If you know you don’t have to worry about validations, use update_all to issue a single UPDATE statement without the costs of instantiating any model objects. # BAD (if you have lots of users) - instantiates all users at once User . all . each do | user | # ... end # GOOD - finds in batches of 1000 User . find_each do | user | # ... end # BAD - find_each ignores limit and order options User . order ( :name ). limit ( 25 ). find_each do | user | # ... end

Did you call a model class from inside your migration? Since your models can change independently of your migration code, if you use your models inside your migrations you can end up writing a migration that works when you run it originally, but that causes errors after someone else changes the model later. For example, you could write a migration like: class SplitUserNames < ActiveRecord :: Migration def up add_column :users , :first_name , :string add_column :users , :last_name , :string User . find_each do | user | user . first_name = user . name . split ( ' ' ). first user . last_name = user . name . split ( ' ' ). last user . save! end remove_column :users , :name end def down add_column :users , :name , :string remove_column :users , :first_name remove_column :users , :last_name end end For now, let’s ignore the various bad assumptions this migration relies on and assume that all the names in the user table are indeed like “Jane Smith”. And let’s say when you wrote this, your user model was empty: class User < ActiveRecord :: Base end You run the migration and it works! You push your code and have a coffee break. Well, someone could come along later and add a validation requiring that all last names be in all caps (again, this is a bad example; don’t actually do this): class User < ActiveRecord :: Base validates :last_name , format: { with: /\A[A-Z]+\z/ } end They add a corresponding migration: class UpcaseUserLastNames < ActiveRecord :: Migration def up User . find_each do | u | u . last_name = u . last_name . upcase u . save! end end def down fail ActiveRecord :: IrreversibleMigration end end Again, let’s ignore that this incorrectly assumes String#upcase will always produce something that satisfies the regular expression /\A[A-Z]+\z/ . They run this migration and it works fine. Once you’ve pulled in their changes, you run their migration which works fine as well. But when you deploy to production, your migration will run first and your call to User#save! will fail because the User class contains a validation on the last_name field that isn’t satisfied until the second migration is run. One way to get around this is to redefine the model at its current state inside the migration: class SplitUserNames < ActiveRecord :: Migration class User < ActiveRecord :: Base end def up add_column :users , :first_name , :string add_column :users , :last_name , :string User . find_each do | user | user . first_name = user . name . split ( ' ' ). first user . last_name = user . name . split ( ' ' ). last user . save! end end def down remove_column :users , :first_name remove_column :users , :last_name end end Now the constant User inside the migration will resolve to the empty class you’ve defined without validations. However, redefining model classes like this can cause problems with polymorphic relations since the User class in this case is actually defined as SplitUserNames::User .