play/type blog

We are creating Germany's juiciest event platform, boomloop.com. Because we love the Internet more than our own mothers. See for yourself. check out boomloop.com


This plugin fixes the rails db: tasks to respect multiple databases, and allows you to place migrations for your other dbs under db/migrate/your-special-db, starting from 0. The plugin aims to be small and non-invasive.

Making Jim Weirich feel a little bit uncomfortable

You might wonder WTF this is supposed to mean, since nobody wants to make Jim Weirich feel even slightly uncomfortable. Let me explain!

When writing this plugin, I tried to use alias_method_chain to stay the hell out of monkey patching land. The other plugin I found that does DB stuff like this had screeds of stuff copy/pasted, and this just sucks in terms of maintainablity.

You don’t get very far with this approach when you’re trying to change existing rake tasks, such as db:migrate. Sure, you can inject tasks before db:migrate like this:

task :nefarious_activities do
  puts "Pinky, are you pondering what I'm pondering?"
end

task :migrate => :nefarious_activities

But what if you want to actually run db:migrate once for each extra db you defined?

Enter alias_task_chain. It can be used just like alias_method_chain, but for tasks. Here’s an example:

  desc "migrates all defined databases."
  task :migrate_with_other_databases => :environment do
    puts "Migrating #{ RAILS_ENV }...\n\n"
    puts %x{ rake db:migrate_without_other_databases }

    for database in Loopy::MultipleDatabases.find_databases do
      all, matching_db, *mop = *RAILS_ENV.match(Loopy::MultipleDatabases::ANY_DB)
      unless matching_db.blank?
        target_environment = "#{ database }_#{ matching_db }"
        puts "Migrating #{ database }...\n\n"
        puts %x{ rake db:migrate_without_other_databases RAILS_ENV=#{ target_environment } }
      end
    end
  end

  alias_task_chain :migrate, :other_databases

I mailed Jim Weirich with this idea, and I think he found it quite unsavoury. I think he said something along the lines of ‘it makes me feel uncomfortable’. I can’t say I can blame him - it kind of creeps me out too.

I can’t think of a more natural way to achieve this though. Any ideas out there?

Tell us what the plugin is about already!

Lets say you have an Analytics DB. Start off by creating a base class like this:

class AnalyticsModel < ActiveRecord::Base
  self.abstract_class = true
  establish_connection("analytics_#{ RAILS_ENV }")
end

now you need to add the appropriate entries in your database.yml:

analytics_development:
  username: analytics
  database: analytics_development
  password: super_secret

analytics_test:
  username: analytics
  database: analytics_test
  password: super_secret

analytics_production:
  username: analytics_prod
  database: analytics_production
  password: super_secret

finally, add environment files to /environments. ie: analytics_test.rb, analytics_production.rb, analytics_development.rb.

db:migrate and db:test:clone have been enhanced to take care of all databases.

Example

With the above setup in place, you can now create a foder under db/migrate for your separate db. In this example, you’d create db/migrate/analytics.

Now create migrations in there, starting with 001-your-first-migration.rb.

You can run all the db tasks for your special dbs by setting RAILS_ENV. For example:

export RAILS_ENV=analytics_development
rake db:schema:dump

this will create db/analytics_development_schema.rb.

rake db:migrate works as normal. if you set RAILS_ENV=production, it will migrate ‘analytics_production’ as well as ‘production’.

Installation

Install the plugin:

./script/plugin install http://svn.playtype.net/plugins/loopy_multiple_databases/

Then add the following lines to your application’s Rakefile.rb, just above require ‘tasks/rails’:

# enhances rake with alias_task_chain method.
module Loopy
  module RakeExtensions
    module AliasTaskChain
      def alias_task_chain(enhancable_task, feature)
        original_name = Rake.application.current_scope.empty? ? 
          target_task : ( Rake.application.current_scope << enhancable_task ).join(":")
        chained_orignal_name = "#{ original_name }_without_#{ feature }"
        chained_enhanced_name = "#{ enhancable_task }_with_#{ feature }"
        target_task = Rake::Task[original_name]
        tasks = Rake.application.send(:eval, "@tasks")
        target_task.instance_variable_set("@name", chained_orignal_name)
        tasks.delete(original_name)
        tasks[chained_orignal_name] = target_task
        task enhancable_task => chained_enhanced_name
      end
    end
  end
end

send :include, Loopy::RakeExtensions::AliasTaskChain