Fri, 12 May 06

Using active record models in rails migrations

I seem to remember that we first came across the “migrations out of sync with code” problems around the time that Scott Laird posted about it regarding typo.

Having just read Ben’s post about rails reliability, I was reminded again about the pitfalls of using model code in migrations.

“Migrations can be problematic if the code under which the migration was written changes before the migration is deployed and released.”

We’re a small team working on the same codebase, yet we still get bitten relatively frequently. A while back we started to re-define our AR classes within our migrations to ensure that we weren’t relying on model code. This has been documented elsewhere. The typo codebase has gone slightly further and created a BareMigration module that takes care of some magic for you. To be fair, I haven’t looked in detail so am not entirely sure what it does..

The problem with re-defining models in the migrations is that it relies on you remembering to re-define them (I’m good at ‘forgetting’ to do things unless I really have to). There is nothing to stop you using the real models; that is, until someone complains that the migrations are broken and you have to fix it.

We are currently using a slightly more robust (I think) solution that forces you to re-define any AR models that you wish to use in migrations. It does this by hijacking the ‘zero’ migration (a migration starting with 0 that gets loaded along with all other migrations but doesn’t interfere in any other way) to ensure any AR derived classes are defined in a module. It’s naive but seems to work ok.

If you don’t already have a zero migration, then create one. The name is important as you will need to create an empty class of the same name in the file itself (for migrations to be happy). Ours is called 000_rails_ext.rb. The code is below. Our code isn’t identical (yet) as I just realised that we had an ineffective call to super in the inherited method, now replaced with alias_method.

class RailsExt
end

class ActiveRecord::Base
  class << self
    alias_method :__original__inherited, :inherited
    def inherited(base)
      raise "You're trying to use an ActiveRecord::Base derived class (--#{base}--) that is not defined in this migration." unless base.to_s =~ /^Migration.*/
      __original__inherited(base)
    end
  end
end

Dispatcher.reset_application!

The inherited callback is triggered everytime we define a class that inherits from ActiveRecord::Base. All we do is check that its name starts with Migration and raise an error if not (failing fast is good). The name is the fully qualified name and so includes namespaces. This is good as it allows us to place all re-defined classes in a module named Migration<number>. This module can then be included in the migration class itself. Although I can’t remember exactly now, I think the reason we need to reset_application! is because we have some calls to our models in environment.rb. These models would already be loaded and therefore any future references to them (in the migrations) would bypass our custom inherited method. By resetting the app, we are sure that we have no AR models loaded.

To re-define the models in the migration, we use something like..

class MyFirstMigration < ActiveRecord::Migration
  def self.up
  end
  def self.down
  end
end

module Migration001
  class Person < ActiveRecord::Base
  end
end
MyFirstMigration.send(:include, Migration001)

Although sending the migration class the include message with the module isn’t great, we use it as it allows us to re-define all our classes below the actual migration. If we were to place the module at the top then this wouldn’t be required but I feel that it gets in the way of being able to read the migration clearly.

A problem with this method and re-defining classes directly in the migration

Both methods create AR derived classes in a namespace that isn’t the root namespace. We came across a problem where we were serializing the contents of one of these re-defined classes to the database from within our migration. When we came to read the serialized data in our code, it didn’t ‘know’ what a Migration001::Person was and so raised an exception. Without thinking about it too much, I think this might cause problems with STI too (I’ve seen notes to this effect from the Typo migration solution).