The ELC Community Blog
A knowledge exchange on Ruby on Rails and Agile Development
Using and Testing Rails with Multiple Databases
by stevend on March 08, 2007
Using multiple databases
I recently wrote a rails plugin called "use_db", which allowed you to use a different database for some of your ActiveRecord models. I started by reading this article (which was borrowed from the rails wiki), and decided to make a plugin out of it. You can use it in the following way: 1 <pre>class SomeBase < ActiveRecord::Base
2 use_db :prefix => "secdb_"
3 self.abstract_class = true
4 end
5
6 class OtherDbModel < SomeBase
7 end</pre>
Now any calls to data in OtherDbModel will go to a database called "secdb_development" (or secdb_test, secdb_production, etc). The database.yml file could have the following additions to support this:
1 <pre>secdb_development:
2 adapter: mysql
3 database: secdb_development
4 username: root
5
6 secdb_test:
7 adapter: mysql
8 database: secdb_test
9 username: root</pre>
Testing multiple databases
One issue with my plugin, as stated on the original article, is that testing becomes very difficult. First, fixtures are automatically inserted into the primary database. Second, other databases will not automatically have their schemas migrated from dev to test.
Solving the fixture problem
I first examined active_record/fixtures.rb and noticed the following problem:
1
2 <pre> def delete_existing_fixtures
3 @connection.delete "DELETE FROM #{@table_name}", 'Fixture Delete'
4 end
5
6 def insert_fixtures
7 values.each do |fixture|
8 @connection.execute "INSERT INTO #{@table_name} (#{fixture.key_list}) VALUES (#{fixture.value_list})", 'Fixture Insert'
9 end
10 end</pre>
These two methods are called automatically by the test helper when loading fixtures for a test. @connection was originally set to ActiveRecord::Base.connection, so the existing solution was not going to work. To solve this, I overrode those two methods in my plugin and replaced them with the following code:
1
2 <pre> alias_method :rails_delete_existing_fixtures, :delete_existing_fixtures
3
4 def delete_existing_fixtures
5 m = get_model
6 return rails_delete_existing_fixtures unless m && m.respond_to?(:uses_db?) && m.uses_db?
7 connection = m.connection
8 connection.delete "DELETE FROM #{m.table_name}", 'Fixture Delete'
9 end
10
11 alias_method :rails_insert_fixtures, :insert_fixtures
12
13 def insert_fixtures
14 m = get_model
15 return rails_insert_fixtures unless m && m.respond_to?(:uses_db?) && m.uses_db?
16 connection = m.connection
17 values.each do |fixture|
18 connection.execute "INSERT INTO #{m.table_name} (#{fixture.key_list}) VALUES (#{fixture.value_list})", 'Fixture Insert'
19 end
20 end</pre>
This first code attempts to get the model associated with a fixture. If found, it asks that model if it uses a different database. FInally, it uses the connection of the model to execute the fixture INSERT and DELETE SQL commands. If any of this process fails, it falls back on the existing rails fixture methods.
Solving the schema migration problem
Rails typically does schema migrations using a rake task which runs before "rake test". It typically divides the work into 3 segments, dump_db_structure, clone_db_structure, and purge_db. The sequence is as follows:
- dump_db_structure dumps the development schema without data to an adapter-specific SQL file
- purge_db deletes all rows from the test database
- clone_db_structure imports the SQL dump into the test database
I simply duplicated the existing rake code, and modified it to use a different database connection. At the end of the day, I could execute a single command to migrate a second database. I chose to execute the command in my test helped in the following manner:
1 <pre>unless defined?(MIGRATED_SEC_DB_FOR_TEST)
2 UseDbTest.prepare_test_db(:prefix => "secdb_")
3 MIGRATED_SEC_DB_FOR_TEST = true
4 end</pre>
The syntax is very similar to the "use_db" helper.
Source code
Download the first release 0.0.1 of use_db rails plugin here.
Timeline
- HTTP Auth with Restful Authentication
- TuneCore covered on TUAW!
- TabTerm Release
- write_inheritable_attribute.............. and friends !
- Liquid Filter Extensions
- Using and Testing Rails with Multiple Databases
- Securing your Models
- Installing RMagick properly in OSX
- RubyGems 0.91 and the "refresh" error
- ActiveRecord attribute update semantics
- Installing Webmin on Ec2
Comments