Rails and RSpec with Two Databases

June 18th, 2015

Maintaining Rails applications in the real world is not always straight-forward.

Recently I needed to enable a Rails 4.0.13 application which used a MySQL database as it’s default to also use a PostgreSQL database. Being a good TDD citizen, I also needed to ensure that the new tests would run in our CI environment.

Whilst I did find a helpful post, it didn’t tell me everything I needed to know. So I thought I’d share what I did.

Database Configuration

First of all, I configured environments for the PostgreSQL database within config/database.yml as follows:

# in addition to existing contents
postgres: &postgresql_defaults
  adapter: postgresql
  pool: 10
  timeout: 60000
  host: localhost
  template: template0

development_foo:
  database: foo_development
  <<: *postgresql_defaults

test_foo:
  database: foo_test
  <<: *postgresql_defaults

staging_foo:
  database: foo_staging
  <<: *postgresql_defaults

production_foo:
  database: foo_production
  <<: *postgresql_defaults

Establish Connection in Model Class

In my case, I only needed to use one model class backed by a table from the extra database. Let’s imaginatively call this class Bar. So the class would be defined like this:

class Bar < ActiveRecord::Base
  establish_connection "#{Rails.env}_foo"

# the rest of the class
end

Note that you probably want to introduce an abstract class which establishes the connection if you have more than one model class which needs it.

Augment rake db tasks

So far so good. The next step was to ensure that the CI script on Buildkite handled the extra database. As you would expect, the script prepares to run the tests by dropping, creating and loading the schema of the database. To do this it uses some code common to all our builds, which includes running bundle exec rake db:drop and bundle exec rake db:create db:schema:load.

I wanted to find a way for the setup of the extra database to piggy-back on these rake tasks and, in so doing, use the existing script unchanged. I discovered I could do so by using the Rake::Task#enhance method. This allowed the rake db tasks to be augmented as follows:

namespace :db do
  task drop_foo: :environment do
    ActiveRecord::Tasks::DatabaseTasks.drop(foo_db_config)
  end

  task create_foo: :environment do
    ActiveRecord::Tasks::DatabaseTasks.create(foo_db_config)
    reset_connection_to_mysql_db
  end

  namespace :structure do
    task load_foo: :environment do
      if File.exist?(foo_structure_filename)
        ActiveRecord::Tasks::DatabaseTasks.structure_load(foo_db_config, foo_structure_filename)
      end
    end
  end
end

def foo_structure_filename
  "#{Rails.root}/db/foo_structure.sql"
end

def foo_db_config
  ActiveRecord::Base.configurations["#{Rails.env}_foo"]
end

def default_db_config
  ActiveRecord::Base.configurations["#{Rails.env}"]
end

def reset_connection_to_mysql_db
  ActiveRecord::Base.establish_connection(default_db_config)
end

Rake::Task['db:drop'].enhance do
  Rake::Task['db:drop_foo'].invoke
end

Rake::Task['db:create'].enhance do
  Rake::Task['db:create_foo'].invoke
end

Rake::Task['db:schema:load'].enhance do
  Rake::Task['db:structure:load_foo'].invoke
end

There are a couple of things I should explain here.

Let’s assume FooApp is the name of application which has foo as it’s default database. FooApp uses SQL rather than Ruby to define it’s schema so I copied db/foo_structure.sql from db/structure.sql in FooApp.

Also worthy of close attention is the call to the reset_connection_to_mysql_db method after the create_foo task. Without this, rake db:schema:load will load the default MySQL schema into the PostgreSQL database!

Conclusion

So there you have it. How to add an extra database to your Rails application and not break CI in the process. Discovering how to use the Rake::Task#enhance method to facilitate this was my main motivation for sharing this.

I hope it helps someone else.

Tags: rspec rake databases rails

Comments

Hi Keith, I have resolved the problem. `ActiveRecord::Tasks` is in Rails 4. Previously, I was in Rails 3. Thanks.

Posted by: zc 4 months later

Hi zc,

It’s difficult to help without a bit more context. If you can create a gist showing the command you ran and the output, I may be able to help.

Keith

Posted by: Keith Pitty 4 months later

I am getting `NameError: uninitialized constant ActiveRecord::Tasks` when using `ActiveRecord::Tasks::DatabaseTasks`. Could you help? Thanks.

Posted by: zc 4 months later

Post a Comment

Please use your real name and be respectful. Textile formatting is supported.