I'm currently working on splitting up a Rails app in different services, to seperate concerns and get some of the benefits of a Service Oriented Architecture (SOA). This app is currently in production and has multiple APIs that needs to remain in place while the new apps are developed.

The challenge

In the new app I have some new models and tables, but I want to reach over to the old app to get a companies table. My config/database.yml looks like this:

common: &common  
              adapter: postgresql
              encoding: unicode
              pool: 10
              host: localhost
            
            development:  
              <<: *common
              database: new_app_dev
            
            test: &test  
              <<: *common
              database: new_app_test
            
            production:  
              <<: *common
              url: <%= ENV['DATABASE_URL'] %>
            

Now, how can I connect to the legacy database in development (and production) to get that companies table for my Company model?

Connecting to the other database

By inheriting from ActiveRecord::Base, an ActiveRecord model automagically connects to the correct database listed in database.yml based on the environment we are running in (development, test or production). In my Company model, I want the same magic, but on a different database. So I decided to copy the database.yml into a config/database_legacy.yml that looks like this:

common: &common  
              adapter: postgresql
              encoding: unicode
              pool: 10
              host: localhost
            
            development:  
              <<: *common
              database: legacy_dev
            
            test: &test  
              <<: *common
              database: legacy_test
            
            production:  
              <<: *common
              url: <%= ENV['LEGACY_DATABASE_URL'] %>
            

It's basically the same as the original database.yml, with other names and a different ENV variable for production (I'll get back to that in another blog post).

Now let's try connecting to the legacy database. Since I'm probably going to need more tables in the coming weeks, I'll start by extracting the calls to the connection into a class called LegacyDatabase:

class LegacyDatabase < ActiveRecord::Base  
              self.abstract_class = true
              databases = YAML.load_file 'config/database_legacy.yml'
              establish_connection databases[Rails.env]
            end  
            

First of all, I inherit from ActiveRecord::Base since I want subclasses to behave like a normal Rails model. When inheriting from ActiveRecord::Base it will try to find a corresponding table in the database for this class, unless it's an abstract_class, so let's set that to true.

Then I load up the new database.yml copy into a hash with

databases = YAML.load_file 'config/database_remote.yml'  
            

This gives me a hash with the environment names as the keys, which I will use to connect to the correct database when calling establish_connection:

establish_connection databases[Rails.env]  
            

This should make sure all connections from LegacyDatabase class goes to the correct database in all environments, so let's make that Company model:

class Company < LegacyDatabase  
            end  
            

Notice that I inherit from my LegacyDatabase abstract class, instead of ActiveRecord::Base directly to get the correct connection for this model. The legacy database already has tables with data from the old app, so I don't want to dump/create that database, or do any migrations to it.

Dump the legacy schema

However, to get my tests up and running (you do have tests right?), I will need a schema.rb to get the tables loaded into the test database. Running rake db:schema:dump will do that for me, however it will only dump the schema for the new database since the task is not aware of my brilliant LegacyDatabase class. Let's extend that rake task by making a lib/tasks/legacy_database.task:

namespace :db do  
              namespace :schema do
                task :dump => [:environment, :load_config] do
                  filename = "#{Rails.root}/db_legacy/schema.rb"
                  File.open(filename, 'w:utf-8') do |file|
                    ActiveRecord::SchemaDumper.dump(
                      RemoteDatabase.connection, file
                    )
                  end
                end
              end
            end  
            

Running rake db:schema:dump again will now give me a db/schema.rb as before, but also a db_legacy/schema.rb with the schema for the legacy database.

Other convenient rake tasks

Some other rake tasks might come in handy, like db:test:prepare and db:create - here's an example of how you can overload the Rails defaults to make your own rake tasks in a legacy namespace:

namespace :legacy do  
              task :setup_paths do
                ENV['SCHEMA'] = 'db_legacy/schema.rb'
                Rails.application.config.paths['db'] = ['db_legacy']
                Rails.application.config.paths['config/database'] = ['config/database_legacy.yml']
              end
            
              namespace :db do
                desc "Create legacy database"
                task :create => :setup_paths do
                  Rake::Task["db:create"].invoke
                end
            
                namespace :test do
                  desc "Load schema for legacy test database"
                  task :prepare => :setup_paths do
                    Rake::Task["db:test:prepare"].invoke
                  end
                end
              end
            end  
            

Now I can run rake legacy:db:create RAILS_ENV=test to create a test database for those legacy models, and rake legacy:db:test:prepare to load that legacy schema I've dumped.

RSpec and database_cleaner

Usually I utilize database_cleaner gem to make sure my test database is in mint condition before running my specs. When I have two databases to clean up, I'm going to need two cleaners to get the job done. This is an extract from my spec_helper.rb:

RSpec.configure do |config|  
              new_cleaner = DatabaseCleaner[:active_record]
              new_cleaner.strategy = :transaction
              legacy_cleaner = DatabaseCleaner[
                :active_record,
                { model: LegacyDatabase }
              ]
              legacy_cleaner.strategy = :transaction
            
              config.before(:suite) do
                begin
                  new_cleaner.start
                  legacy_cleaner.start
                  FactoryGirl.lint
                ensure
                  new_cleaner.clean
                  legacy_cleaner.clean
                end
              end
            
            end  
            

This will make sure the database looks the same everytime I run my tests. Note that Rails 4.1 deprecated rake db:test:prepare for having ActiveRecord::Migration.maintain_test_schema! in your spec_helper.rb. This will automatically keep your test database schema up-to-date with the development schema, but only for the default database. Should your legacy database schema change (let's hope not), you will have to dump and load the schema with the rake tasks manually.

Happy hacking :)