Safe data migrations

Published at: 9.IV.2008 05:02 CEST
Categories: english, plugin, rails

Something ago I wrote about the problems which arise when using models in your Rails migrations. Meanwhile I developed a really simple solution to this problem and today I wrapped it up in to a plugin called SafeDataMigrations.

How to use it? Install it in your Rails application:

ruby script/plugin install http://svn.remvee.net/plugins/safe_data_migrations

and apply it in your migration:

class DowncaseUserNames < ActiveRecord::Migration
  models :user
  
  def self.up
    User.find(:all).each do |user|
      user.update_attributes(:names, user.name.downcase)
    end
  end

Look at the README file for a more elaborate example.

So how does it work? It simply undefines the models your referring to and redefines an empty ActiveRecord class;

Object.send :remove_const, :User rescue nil
class User < ActiveRecord::Base; end

Now you are sure to have a User model available in your migration without any validations which may make data manipulations impossible. The undefining of an already available model also ensures you don’t need to use ActiveRecord::Base.reset_column_information before updating new fields, unless you use your model before altering the table of course.

Update: As coderrr points out you don’t need to clobber the global scope model class because a nested model works fine too. I wrongly assumed introducing a new model class in side (!) a migration class would only reopen my original model class and keep validations intact. To illustrate:

class User; def top; end; end

class Migration
  class User; def nested; end; end
  
  def self.go
    p User.instance_methods - Object.instance_methods
  end
end

Migration.go

yields ["nested"] and not ["top", "nested"] as I suspected. Apparently I was bitten too hard by a problem which arose when I used an original model class to even try the above. I’ll pull the plugin because it’s pointless.. Bad me, thanks coderrr!

Marcel @ about 2 hours

Nice (and useful!) abstraction applied in our last project (where Remco came up with this when we were dealing with lots of data migrations).
I’m sure this little piece of code has — and, in the future, will — save me a lot of trouble. Thanks!

Marcel @ about 2 hours

Lol, apparently your blog supports some sort of formatting. My stylish double-dash applied a strike-through to my text…

coderrr @ about 13 hours

I don’t think you need to remove and redefine the class in the global scope. You can instead just define the class inside of your migration class:

class SomeMigration < AR::M
  class User < AR::B
    has_many :roles
  end
  class Role < AR::B
    belongs_to :user
  end

  def self.up
    ...
    User.find(:all).each{|u| u.roles.create(...) }
  end
end

The benefit of this is that the original model class is unaffected. Was there some case you found where this did not work?

http://coderrr.wordpress.com

Remco @ about 21 hours

Digging through commit logs I noticed I didn’t even try a nested class assuming I would just reopen the original. Actually I was so freaked by the problems my migration caused I even moved to using SQL for a while. How embarrassing.. Thanks for the heads up!