In the real world, application requirements change all the time. Features get cut, new features get added, things improve and change; sometimes, initial requirements become completely obsolete and need to be thrown out.
On a more microscopic level, your application models change. Classes change–you might need to add fields, drop fields, change fields, and so on.
So the question arises–how can you not only make these changes (in a database-independent fashion), but also track these changes so that you can apply them to your production database, version them, and potentially roll them back?
The answer lies in the database migration tool, which you may have seen: rake db:migrate
The nitty-gritty details of rake are not so important right now; what is important is that whenever you create a model, Rails also creates a new database migration record, which you can apply, apply to another database (eg. production), or roll back.
Say your application models a blog, and you have a notion of comments. When you create the model via ruby script/generate model comments, Rails will tell you something like:
exists app/models/
exists test/unit
exists test/fixtures
create app/models/comment.rb
create test/unit/comment_test.rb
create test/fixtures/comments.yml
create db/migrate
create db/migrate/002_create_comments.rb
The last two lines are the ones we’re interested in. Notice comments are created with the number 002; presumably, the first model was posts. If you open up 002_create_comments.rb, you’ll see something like:
class CreateComments < ActiveRecord::Migration
def self.up
create_table :comments do |t|
end
end
def self.down
drop_table :comments
end
end
Ruby calls self.up when we migrate, and calls self.down when we roll back. All you need to do are add the columns (model fields) you want into self.up, and life is good. You might fill in something like this:
def self.up
t.column :author, :string
t.column :content, :text
t.column :created_on, :date
end
This adds three fields to our model--an author (a short string), comment content (an arbitrary-length text-field), and the date field created_on, which Rails will automatically update for us.
Once that's done, to apply your changes, simply type rake db:migrate, and you're done! (The current version of your application resides in the schema_info table.)
What if you want to roll back? Simply type rake db:migrate VERSION=1, and Rails will run the self.down code, and trash the comments table.
Let's say a week goes by, and you start to receive nasty comments on your blog. You decide that the best way to deal with it is first to start tracking who's commenting--not by author, but by IP!
How can you add the IP field your comment model? Rather then directly altering the table in SQL, you can create a new clean, versionable migration, like so:
ruby script/generate migration add_ip
#ruby will say ...
exists db/migrate
create db/migrate/003_add_ip.rb
Notice the migration is number 003; your current version, after adding comments, is 002. Fire up the 003_add_ip.rb file; like the comments file, it has self.up and self.down. This time, self.up should add the IP field, and self.down should remove it, like so:
def self.up
add_column :comments, :ip, :string, :null
end
def self.down
remove_column :comments, :ip
end
Notice we can specify more detail about the column; here, we specify that IP is a string, and can be null. (For a full list of the type of commands and options you can use in a migration, check the ActiveRecord::Migration API page.)
Run the migration tool again: rake db:migrate and tada! You now have an IP column on your comments table! (Note that you'll have to add some extra code in your comment forms so that it automatically picks up the IP and puts it into a hidden input field.)
I have tried to use db:migrate to migraet the second model. But when i run the migration it thinks im trying to migrate the first model and says that it already exists.
Help!
Thanks
Hi Chris,
If you’re not averse to undoing the first migration, you can do this to destroy and redo everything:
rake db:migrate version=0 (this will undo all migration)
rake db:migrate (this will migrate to the max)
You can always try specific migrations somehow, but it seems like your DB has the wrong version recorded, so you might want to un-migrate and re-migrate. (Be aware that it’ll drop the table, so if your data is valuable to you, you’d better back it up first!)