I just started using the migrations feature of Ruby on Rails. Migrations are a way to capture changes to a database schema so that you can apply them without destroying data, and so that you can roll them back if you made a mistake. Before I understood how they worked, I thought they were basically magical and was hesitant to try them out because I don’t believe in magic. Some Rails developers I’m working were extolling them, so I investigated further and discovered that they are not magic, but that they are pretty darn useful.
One thing you learn about Rails is that just about every file you touch is a full blown Ruby script, whether it’s a unit test, a build file (or Rakefile as they’re known in Rails world), or a database migration script. The only thing that differentiates them is how they’re used and which libraries they import. So a migration script is just a Ruby script that has access to a bunch of methods that perform database operations. It also has access to all of the model classes in your application, so that you can modify the data in your database from within a migration as well. Fundamentally, a migration script isn’t all that different than a file full of SQL statements that modify a database schema, except for perhaps a small usability improvement.
Each migration script has two methods that it has to override, one to apply the changes in the migration and another to roll them back. So if you want to add a new table called “widgets” to your database, in your migration script you add the code to create the table to the “up” method, and the code to drop the table in the “down” method.
As I said, there’s nothing magical about the migration files themselves. They’re just collections of database operations. What’s more interesting is how Ruby on Rails applies them. Migration files are versioned, and the version number is part of the file name. The first migration has a name like
001_initial_schema.rb. You apply it using the command
rake migrate. So if another developer checks in versions two through five of the schema, the next time you run
rake migrate it will apply each of the migrations in turn to bring your schema up to the most recent revision. Of course you can also use it when deploying to production, eliminating a lot of risk that comes from hacking on a live schema manually to apply changes that were made since the last version was deployed.
All in all, migrations are an elegant solution to a problem that’s common to all Web applications, regardless of the problem that they’re written in. They could easily be ported to Ant for Java applications, or to Perl or PHP scripts for those platforms as well. Maybe I’m missing the fact that such tools are already common, but I haven’t seen them.
The only question for me with migrations is how to best include them in my workflow. For example, let’s say your adding a new feature to your application that requires you to add a column called “color” to a table called “widgets”. Do you generate a new migration script (Rails comes with a generator that will automatically build a skeleton of a migration script for you with the proper version number), add the command to add the column to that migration script, and then run
rake migrate, or do you modify the schema yourself and just create the migration before you check your code in?
If you do your initial work in the migration script, the problem is that if you add more stuff later, you have to roll back the migration and then run it again to apply all of the changes to your development database. On the other hand, if you make the schema changes by hand, you increase the risk of checking in a migration that has not been well tested. After some testing, it seems like the best approach is to create one migration for each change you come up with? Let’s say you’re adding a comments feature to a blog, so you write a migration that creates the table for comments and run the migration on your local system. You realize later that you need to store the URL of the commenter. The best approach, in terms of using migrations most efficiently, is to create a new migration to add that field rather than editing the previous migration. At least that’s how it seems to me.