Exodus - A migration framework for MongoDb
A migration Framework for a schemaless database?
After working with Mongo for long time now I can tell you working with a schemaless database does not mean you will never need any migrations. Within the same collection Mongo allows to have documents with a complete different structure, however in some case is you might want to keep data consistency; Especially when your code is live in production and used by millions of users.
There is a plenty of way to modify documents data structure and after a deep reflexion I realized it makes more sens to use migration framework. A migration framework provides a lot of advantages, such as:
- It allows you to know at any time which migration has been ran on any given system
- It's Auto runnable on deploy
- When switching enviromment (dev, pre-prod, prod) you don't need to worry if the script has been ran or not. The framework takes care of it for you
Exodus has been used in production since March 2013.
Installation
Add this line to your application's Gemfile:
gem 'exodus'
And then execute bundle install:
$ bundle
Or install it yourself as:
$ gem install exodus
Configuration
You need to configure 4 things before using Exodus: the database name, the mongodb connection, the config file location and the path to the directory that will include your migrations:
require 'exodus'
Exodus.configure do |config|
config.db = 'migration_db'
config.connection = Mongo::MongoClient.new("127.0.0.1", 27017, :pool_size => 5, :pool_timeout => 5)
config.config_file = File.dirname(__FILE__) + '/config/migrations.yml'
config.migrations_directory = File.dirname(__FILE__) + '/models/migrations'
end
Then you just need to loads the migrations tasks by adding the following line to your rakefile:
load Exodus.tasks
... And you're all set!
Basic knowledge
- All Migrations have to be ruby classes that inherits from Migration class.
- Migrations have a direction (UP or DOWN)
- UP means the migration has been migrated
- DOWN means the migration has not been run or has been rollbacked
- All migrations have a current_status and status_complete
- When current_status is equal to 0 it means the migration has not been run or has been succesfully rollbacked
- When current_status is equal to status_complete it means the migration has been succefully migrated
- We decided to keep track of migration by enumerating them.
- Migrations will run in order using the migration_number
- Migrations can be rerunnable safe, rerunnable safe migrations will run on each db:migrate even if the migration has already been run!
To Do when writting your own
- Give it a migration_number
- Initialize it and define status_complete and description
- Write the UP method that will be call when migrating the migration
- Write the DOWN method that will be call when rolling back the migration
- If your migration contains distinct steps that you want to split up I recommand using the "step" DSL
Good example
class RenameNameToFirstName < Exodus::Migration
self.migration_number = 1
def initialize(args = {})
super(args)
self.status_complete = 3
self.description = 'Change name to first_name'
end
def up
step("Creating first_name index", 1) do
puts Account.collection.ensure_index({:first_name => 1}, {:unique => true, :sparse => true})
end
step("Renaming", 2) do
puts Account.collection.update({'name' => {'$exists' => true}}, {'$rename' => {'name' => 'first_name'}},{:multi => true})
end
step("Dropping name index", 3) do
puts Account.collection.drop_index([[:name,1]])
end
self.status.message = "Migration Executed!"
end
def down
step("Creating name index", 2) do
puts Account.collection.ensure_index({:name => 1}, {:unique => true, :sparse => true})
end
step("Renaming", 1) do
puts Account.collection.update({'first_name' => {'$exists' => true}}, {'$rename' => {'first_name' => 'name'}},{:multi => true})
end
step("Dropping first_name index", 0) do
puts Account.collection.drop_index([[:first_name,1]])
end
self.status.message = "Rollback Executed!"
end
end
Commands
db:migrate
Executes all migrations that haven't run yet. You can the STEP enviroment to run only the first x ones.
rake db:migrate
rake db:migrate STEP=2
db:rollback
Rolls back all migrations that have already run. You can set the STEP enviroment variable to rollback only the last x ones.
rake db:rollback
rake db:rollback STEP=2
db:reset
Rolls back all migrations that have already run and then run all migrations.
rake db:reset
db:migrate:show
Print in the console all migrations that rake db:migrate will run. Does NOT run any migration. You can the STEP enviroment to run only the first x ones.
rake db:migrate:show
rake db:migrate:show STEP=2
db:rollback:show
Print in the console all migrations that rake db:rollback will run. Does NOT run any migration. You can set the STEP enviroment variable to rollback only the last x ones.
rake db:rollback:show
rake db:rollback:show STEP=2
db:migrate:custom
Executes all custom migrations that haven't run yet. Custom migrations will be loaded from your config file. Custom migrations will run in order of appearence. You can set the STEP enviroment variable to rollback only the last x ones.
rake db:migrate:custom
rake db:migrate:custom STEP=2
db:rollback:custom
Executes all custom migrations that haven't run yet. Custom migrations will be loaded from your config file. Custom migrations will run in order of appearence. You can set the STEP enviroment variable to rollback only the last x ones.
rake db:rollback:custom
rake db:rollback:custom STEP=2
db:migrate:list
Lists all the migrations.
rake db:migrate:list
db:migrate:status
Gives a preview of what as been run on the current database.
rake db:migrate:status
db:migrate:yml_status
Prints on the screen the content of the yml configuration file
rake db:migrate:yml_status
db:mongo_info
Prints on the screen information about the current mongo connection
rake db:mongo_info
Namespacing
You might be using other gems in your project that uses rake db:migrate
or rake db:rollback
. In order to avoid conflicts you can define rake_namespace, in the Ruby code:
Exodus.configure do |config|
config.rake_namespace = 'mongo'
end
Or in your Yaml file:
migration:
rake_namespace: 'mongo'