toolmantim

Migrating with Models

February 24, 2006 00:13 (Sydney Australia)

No, I’m not talking about marrying an international hottie. I’m talking about using or, rather, not using ActiveRecord models in your migrations (though I think for the former topic may have been incredibly more interesting and less geeky. Maybe another time.)

Back in October Scott Laird blogged about Migrating in two dimensions and the problems people had running Typo’s migrations.

Basically, it boils down to one thing: Don’t use your ActiveRecord models in your migrations or your life will be miserable.

...but why, why must you say these things? Because who knows what your class will look at some point in the future when your migration is run. Methods might change, bacon will cease to be chunky… hell, the model might not even exist.

...ok, so what the hell are we supposed to do? Good question.

Option One: Only use straight SQL… don’t use an ActiveRecord magic. Well duh… but I didn’t learn this rails thing fo nothing. Let me use some of my ActiveRecord mojo, baby!

Option Two: Create temporary ActiveRecord classes to go along with your migration. Oh yeah, now that’s what I’m talkin about.

For example, in a project I’m currently working on I wanted to add acts_as_list capability to the Product model so needed to add a ‘position’ attribute. In the migration I also wanted to assign a default position to all the model instances.

One possible implementation could have been:

class AddPositionToProducts < ActiveRecord::Migration
  def self.up
    add_column :products, :position, :integer
    Product.reset_column_information

    # Set default list orders
    SoftwareProduct.find(:all).inject(1) do |i,p|
      p.update_attribute(:position,i)
      i+1
    end
    CourseProduct.find(:all).inject(1) do |i,p|
      p.update_attribute(:position,i)
      i+1
    end
  end

  def self.down
    remove_column :products, :position
  end
end

Not so fast cowboy.

Say in a few days time I do a big refactor and remove the Product class altogether. What happens two months from now when this migration is run? Boom!

The solution is to create classes specific to each migration.

class AddPositionToProducts < ActiveRecord::Migration
  class Product < ActiveRecord::Base; end
  class SoftwareProduct < Product; end
  class CourseProduct < Product; end

  def self.up
    add_column :products, :position, :integer
    Product.reset_column_information

    # Set default list orders
    SoftwareProduct.find(:all).inject(1) do |i,p|
      p.update_attribute(:position, i)
      i+1
    end
    CourseProduct.find(:all).inject(1) do |i,p|
      p.update_attribute(:position, i)
      i+1
    end
  end

  def self.down
    remove_column :products, :position
  end
end

Wallah! No external code dependencies. Nobody can break our migration… no matter what they can do to the code in /app/models/. And we still get to use our ActiveRecord mojo.

Comments

Tim Lucas

and the same discussion was going on in the rails-core mailing list a few days ago

Tim Lucas

btw if you are initialising positions like above you want to start them from 1, not 0 (i.e. Model.find(:all).inject(1) { ... })

Anonymous

Great article. Definitely something I can use in my development.

p.s. It’s Voila, not Wallah! ;-P

charlie bowman

I hadn’t even thought about that! That’s definately something to think about if you’re working with open source, since it changes so frequently.

Ben Askins

If only I was subscribed to your blog back in feb 2006. I could have avoided some grief over the last couple of days.

Tim Lucas

Well at least you found it when you had the grief! The fact that the migration documentation encourages you to use your models doesn’t help either.

Alex Fetcher

Great site!
29ded3f4d82ae5e6082a52c9b8306a80

To comment on this article you must have javascript enabled.