Tue 5 Aug 2008
I got sucked into a trap. In a Rails migration it’s so easy to use an ActiveRecord::Base model object to update some stuff, and it looks nice and DRY. But it is extremely fragile. For example suppose your domain has a user and sponsor model. Each user has a sponsor. You start out this way:
def self.up
create table :users do |t|
t.string :name
t.string :password
t.string :sponsor_name
t.timestamps
end
end
Later you want to insist that every user in the system have a sponsor. You already have a bunch of test data that you are intellectually invested in, so you whip up a quick migration to make sure.
def self.up
User.find(:all, :conditions => 'sponsor_name IS NULL').each do |u|
u.sponsor_name = "The Default"
u.save!
end
end
Cool. Works good. You can go up and down. But some time later you realize the supreme error of your ways. You should have had a sponsor model and the user should have a sponsor_id, not a sponsor_name. Doh! What was I thinking. Time for another migration:
def self.up
add_column :users, :sponsor_id, :integer
drop_column :users, :sponsor_name
end
and the corresponding self.down method too, and the necessary belongs_to, has_one in the user and sponsor model classes. But now you can’t migrate the database up from bootstrap any more because your use of the the User model with u.sponsor_name above will not work. The user no longer has a sponsor_name.
The root of the issue is that the model objects are not versioned along with the database as it goes up and down. Since fixtures just use the same model objects that doesn’t help one bit.
The only reliable way we have come up with is to inject data using raw sql. So step two above
could change to
def self.up
execute 'update users set sponsor_name = "The Default" where sponsor_name IS NULL'
end
That works, up and down the line, because the database sql IS in sync with the database at every step.
Have a better way to write less brittle migrations? Let me know in the comments.