In my “Why all the hype over Ruby on Rails?” post I mentioned that one of my favorite features within Ruby on Rails is migrations. Since migrations seems to be one of the features within Rails that many of my .NET buddies don’t know about I figured I’d post about them to not only help expose people to what migrations are but also to help make sure I can explain them in a coherent way.
What’s a migration?
As we develop applications we continuously refactor our code and database to add new functionality as well as to clean up what is already in place. As we add features to our applications we modify the schema of the database that we use to house our data. I’ve used many different methods for managing this process with varying degrees of success.
When modifying a database schema developers often create SQL files that perform the necessary operations on their database. The SQL file is meant to take a previous version of a database up to a subsequent version. Since SQL files usually have keywords specific to a database platform they are not re-usable for other database platforms. If you support more then one database platform this can result in multiple SQL files for a single change. What ever happened to that DRY principle that the Pragmatic Programmers advocate?
One of the other problems that we face is the process is rolling out the changes we make to the other members of our team. At my old place of employment we used a DTS package to copy the current “checked-in” version of the database to each developers local machine. Although the process worked it wasn’t as smooth as one would hope.
Ruby on Rails migrations is meant to solve the problem of rolling out changes to your database. By defining the changes to your database schema in Ruby files development teams can ensure that all changes to the database are properly versioned. Additionally migrations help to ensure that rolling out changes to fellow developers as well as other servers (development, QA, production) is handled in a consistent and manageable fashion.
What are we migrating?
Before moving onto a definition of how migrations work I’d like to overview what types of changes we as developers make to our database.
- Adding a new table
- Removing a table
- Adding a column to an existing table
- Removing a column
- Renaming a column
- Renaming a table
- Adding an index
- Adding a primary key
- Adding a foreign key
Defining a change in Ruby
One of the nice things about Ruby is that it can feel very much like a domain specific language. By using ruby as a domain specific language we can define the changes we make to our database in a way that abstracts that change away from a single database platform. This allows the change to be applied to not only other databases but also to completely different database platforms. For example to create a new table using migrations you can create the following migration file:
class AddUserTable < ActiveRecord::Migration
def self.up
create_table :users do |table|
table.column :name, :string
table.column :login, :string
table.column :password, :string, :limit => 32
table.column :email, :string
end
end
def self.down
drop_table :users
end
end
As you can see the migration file above has two methods, up and down. Migrations not only support migrating a database from a previous version to the current version but it also supports rolling back database changes. So if I was currently on version 5 of our database and the above migration was to move to version 6 I could very easily roll back the version 6 change by saying “yo give me version 5 fool!”. When moving up a version the logic within the “up” method is called, and when rolling back to a previous version the “down” method is executed.
Migrations also support other changes such as:
class MakeAllKindsOfChanges < ActiveRecord::Migration
def self.up
add_column :users, :phone_number, :string, :limit => 20
remove_column :users, :email
change_column :users, :name, :limit => 10
drop_table :some_old_table
rename_column :users
rename_table :users, :myCoolUsersTable
end
def self.down
#undo all that stuff I just did above
end
end
By utilizing migrations I can define every database change in an abstract fashion and then rely on the various connection adapters to convert my change into the SQL necessary for each database platform (MySQL, PostgreSQL, SQLite, SQL Server, and Oracle).
Applying a change
After I’ve defined all my changes in my migration files I can then update my database by using the “rake migrate” command. I can tell migrate to use a specific database and I can tell it what specific version I want my database migrated to by adding the VERSION option:
rake migrate VERSION=5
Summary
There is actually a lot more that could be said but this post is already longer then I wanted so let’s wrap up. Rails Migrations allow developers to define the changes to their database using a domain specific language that abstracts the change away from a single database platform. It tracks the changes using versions and allows for developers to migrate a database to any given version. Additionally migrations can be used to update both developers workstations as well as to apply changes to production databases.
Command reference:
dumps an existing database to a Rails schema file which allows you to port an application to different database platforms.
rake db_schema_import – imports the schema dumped using db_schema_dump
rake migrate – migrates a database to the current version
rake migrate VERSION={X} – migrates a database to a specific version
rake environment RAILS_ENV=production migrate – migrates the production database to the current version
rake schema_generator – products a .SQL file for each supported database platform (not everything is currently supported)