Nikolay Sturm's Blog

Musings about Development and Operations

Multi-Database Setup With Rails and RSpec

| Comments

Last week I had to integrate one of our Rails applications at work with an additional database. While the Rails parts of the setup are quite well documented on the net, my Google Fu failed me when it came to making it work with RSpec. So here are my findings…

Integration with Rails

Adding additional databases to a Rails application is quite simple, all it takes is a database connection in database.yml and establishing the proper connection in associated models.

Let’s say we want to integrate the foo database, the additional connection might be:

1
2
3
4
5
6
foo:
  adapter: mysql2
  database: foo
  encoding: utf8
  username: root
  password: secret

To access the foo database, we conventionally use models in the Foo namespace and use a proper connection.

1
2
3
class Foo::Bar < ActiveRecord::Base
  establish_connection 'foo'
end

DRY it up

That’s almost it for the Rails part. At this point our application can access the foo database, but we may start wondering if we could somehow dry up database.yml, as it now contains at least three very similar entries.

The answer is merge keys. This slightly esoteric feature of YAML allows one block to include definitions from another block. In our case all our connections are identical, except for the database they use. Let’s create a block with common directives and merge it into each connection.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
defaults: &defaults
  adapter: mysql2
  encoding: utf8
  username: root
  password: secret

development:
  database: development
  <<: *defaults

test:
  database: test
  <<: *defaults

foo:
  database: foo
  <<: *defaults

That looks much cleaner, time to setup the database for our tests.

Integration with RSpec

In order to get reproducable test runs, the database used by test cases has to be in a defined state at all times. Rails ensures this property in principle by separating the test database from the development database via the Rails environment. The test database can easily be purged and should be managed by the test framework.

If we want to write tests against models using our new foo database, we should reproduce Rails’ setup and use one database in the development environment and a different one in the test environment.

Lets start with the model. The easiest way to differentiate between Rails environments is encoding it in the database connection. So we change our model to:

1
2
3
class Foo::Bar < ActiveRecord::Base
  establish_connection "foo_#{Rails.env}"
end

Now we have to adapt database.yml accordingly:

1
2
3
4
5
6
7
8
...
foo_development:
  database: foo
  <<: *defaults

foo_test:
  database: foo_test
  <<: *defaults

Having decoupled the development database from the test database, there is only one more issue: the test database is completely empty. We have to find a way to manage the schema.

Rails uses rake tasks for database management. The tasks most often used are rake db:migrate for updating the development database and dumping its schema and rake db:test:prepare to clean and update the test database. It would be nice if we could adapt these tasks to manage foo_test as well, so we don’t have to remember any new tasks.

A neat feature of rake is the possibility to define tasks multiple times, in effect adding code to existing tasks. In our case, we can extend db:schema:dump and db:test:load_schema to manage the foo_test database and completely hide this fact from the developer.

Let’s put the following code into lib/tasks/db.rake:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
namespace :db do
  namespace :schema do
    # desc 'Dump additional database schema'
    task :dump => [:environment, :load_config] do
      filename = "#{Rails.root}/db/foo_schema.rb"
      File.open(filename, 'w:utf-8') do |file|
        ActiveRecord::Base.establish_connection("foo_#{Rails.env}")
        ActiveRecord::SchemaDumper.dump(ActiveRecord::Base.connection, file)
      end
    end
  end

  namespace :test do
    # desc 'Purge and load foo_test schema'
    task :load_schema do
      # like db:test:purge
      abcs = ActiveRecord::Base.configurations
      ActiveRecord::Base.connection.recreate_database(abcs['foo_test']['database'], mysql_creation_options(abcs['foo_test']))
      # like db:test:load_schema
      ActiveRecord::Base.establish_connection('foo_test')
      ActiveRecord::Schema.verbose = false
      load("#{Rails.root}/db/foo_schema.rb")
    end
  end
end

Now, whenever we run a rake task that results in a schema dump, foo’s schema will be dumped to db/foo_schema.rb as well. Whenever we load a schema into our test database, foo_test will be updated, too.

That’s it, we are done. Time to write some specs…

Comments