7 min read
Scaling Rails Applications with Multiple Databases: A Complete Guide
Ruby on RailsPostgreSQLScaling
As Rails applications grow in complexity and scale, managing all your data in a single database can become a bottleneck. Whether you're dealing with massive datasets, need to separate read and write operations, or want to isolate different business domains, Rails' multiple database support provides elegant solutions to these challenges.
In this comprehensive guide, we'll explore how to configure, implement, and optimize multiple databases in your Rails application.
Why Use Multiple Databases?
Before diving into implementation, let's understand the key scenarios where multiple databases make sense:
Read/Write Splitting: Distribute read operations across replica databases while keeping writes on the primary database. This improves performance and reduces load on your main database server.
Domain Separation: Isolate different business domains or microservices within a monolithic application. For example, keeping user authentication data separate from analytics data.
Legacy Integration: Connect to existing databases when migrating from legacy systems or integrating with third-party applications.
Performance Optimization: Distribute heavy analytical queries to dedicated databases, keeping your main application database responsive.
Setting Up Multiple Databases in Rails
Rails 6.0 introduced native support for multiple databases, making configuration straightforward. Let's start with the basic setup.
Database Configuration
First, update your config/database.yml to define multiple databases:
default: &default
adapter: postgresql
encoding: unicode
pool:
username:
password:
host:
development:
primary:
<<: *default
database: myapp_development
analytics:
<<: *default
database: myapp_analytics_development
users:
<<: *default
database: myapp_users_development
production:
primary:
<<: *default
database: myapp_production
analytics:
<<: *default
database: myapp_analytics_production
users:
<<: *default
database: myapp_users_productionApplication Configuration
Configure your application to recognize the multiple databases by updating config/application.rb:
module MyApp
class Application < Rails::Application
config.load_defaults 7.0
# Configure multiple databases
config.active_record.multiple_databases = true
end
endCreating Database-Specific Models
With multiple databases configured, you need to tell your models which database to use. Rails provides several approaches for this.
Abstract Base Classes
Create abstract base classes for each database:
# app/models/application_record.rb
class ApplicationRecord < ActiveRecord::Base
self.abstract_class = true
connects_to database: { writing: :primary, reading: :primary }
end
# app/models/analytics_record.rb
class AnalyticsRecord < ActiveRecord::Base
self.abstract_class = true
connects_to database: { writing: :analytics, reading: :analytics }
end
# app/models/users_record.rb
class UsersRecord < ActiveRecord::Base
self.abstract_class = true
connects_to database: { writing: :users, reading: :users }
endModel Implementation
Now create your models inheriting from the appropriate base class:
# app/models/product.rb
class Product < ApplicationRecord
has_many :orders
validates :name, presence: true
end
# app/models/analytics/page_view.rb
class Analytics::PageView < AnalyticsRecord
validates :url, presence: true
validates :user_id, presence: true
end
# app/models/user.rb
class User < UsersRecord
has_secure_password
validates :email, presence: true, uniqueness: true
endDatabase Migrations for Multiple Databases
Managing migrations across multiple databases requires some additional setup.
Migration Structure
Organize your migrations by database:
db/
├── migrate/ # Primary database migrations
├── analytics_migrate/ # Analytics database migrations
└── users_migrate/ # Users database migrationsMigration Configuration
Update your migration paths in config/application.rb:
config.paths.add "db/analytics_migrate", glob: "*.rb"
config.paths.add "db/users_migrate", glob: "*.rb"Running Migrations
Create a custom rake task to handle migrations across all databases:
# lib/tasks/db_multi.rake
namespace :db do
namespace :multi do
desc "Run migrations for all databases"
task migrate: :environment do
ActiveRecord::Base.connected_to(database: :primary) do
Rake::Task["db:migrate"].invoke
end
ActiveRecord::Base.connected_to(database: :analytics) do
ActiveRecord::MigrationContext.new("db/analytics_migrate").migrate
end
ActiveRecord::Base.connected_to(database: :users) do
ActiveRecord::MigrationContext.new("db/users_migrate").migrate
end
end
end
endRead/Write Splitting Implementation
One of the most common use cases for multiple databases is implementing read/write splitting for better performance.
Configuration for Read Replicas
Update your database.yml to include read replicas:
production:
primary:
replica: trueModel Configuration
Configure your models to use read replicas:
class ApplicationRecord < ActiveRecord::Base
self.abstract_class = true
connects_to database: {
writing: :primary,
reading: :primary_replica
}
endAutomatic Read/Write Splitting
Rails automatically routes reads to replica databases when configured:
# This will use the primary database (write)
User.create(name: "John", email: "john@example.com")
# This will use the replica database (read)
User.where(active: true).limit(10)
# Force writing database for reads when needed
User.connected_to(role: :writing) do
User.find(params[:id])
endCross-Database Associations and Queries
Working with data across multiple databases requires careful consideration of associations and queries.
Handling Cross-Database Associations
Rails doesn't support associations across databases directly. You'll need to implement custom solutions:
class Product < ApplicationRecord
def page_views
Analytics::PageView.where(product_id: id)
end
def total_views
page_views.count
end
end
class Analytics::PageView < AnalyticsRecord
def product
Product.find(product_id)
end
endService Objects for Cross-Database Operations
Create service objects to handle complex cross-database operations:
class ProductAnalyticsService
def self.product_performance(product_id)
product = Product.find(product_id)
analytics_data = Analytics::PageView
.where(product_id: product_id)
.group(:date)
.count
{
product: product,
views: analytics_data,
total_views: analytics_data.values.sum
}
end
endConnection Management and Performance
Proper connection management is crucial when working with multiple databases.
Connection Pool Configuration
Configure appropriate connection pools for each database:
production:
primary:
<<: *default
database: myapp_production
pool: 25
analytics:
<<: *default
database: myapp_analytics_production
pool: 10 # Fewer connections for analyticsManual Connection Management
When you need fine-grained control over connections:
class ReportGenerator
def generate_cross_database_report
primary_data = nil
analytics_data = nil
ActiveRecord::Base.connected_to(database: :primary) do
primary_data = Product.joins(:orders)
.group(:category)
.sum(:price)
end
ActiveRecord::Base.connected_to(database: :analytics) do
analytics_data = Analytics::PageView
.group(:product_category)
.count
end
merge_report_data(primary_data, analytics_data)
end
endTesting with Multiple Databases
Testing applications with multiple databases requires additional setup.
Test Database Configuration
Configure test databases in database.yml:
test:
primary:
<<: *default
database: myapp_test
analytics:
<<: *default
database: myapp_analytics_test
users:
<<: *default
database: myapp_users_testTest Setup
Create a helper to manage test database setup:
# test/support/database_helper.rb
module DatabaseHelper
def setup_test_databases
ActiveRecord::Base.connected_to(database: :primary) do
ActiveRecord::Schema.define do
# Primary database schema
end
end
ActiveRecord::Base.connected_to(database: :analytics) do
ActiveRecord::Schema.define do
# Analytics database schema
end
end
end
endFactory Setup
Configure factories to work with multiple databases:
# test/factories/products.rb
FactoryBot.define do
factory :product do
name { "Sample Product" }
price { 19.99 }
trait :with_analytics do
after(:create) do |product|
ActiveRecord::Base.connected_to(database: :analytics) do
create(:page_view, product_id: product.id)
end
end
end
end
endMonitoring and Maintenance
Monitoring multiple databases requires additional consideration.
Health Checks
Implement health checks for all databases:
class DatabaseHealthCheck
def self.check_all
databases = [:primary, :analytics, :users]
databases.map do |db|
{
database: db,
status: check_database(db),
connection_count: connection_count(db)
}
end
end
private
def self.check_database(db)
ActiveRecord::Base.connected_to(database: db) do
ActiveRecord::Base.connection.execute("SELECT 1")
'healthy'
end
rescue => e
'unhealthy'
end
endPerformance Monitoring
Monitor query performance across databases:
class DatabaseMetrics
def self.slow_queries_by_database
databases = [:primary, :analytics, :users]
databases.each_with_object({}) do |db, metrics|
ActiveRecord::Base.connected_to(database: db) do
metrics[db] = slow_query_count
end
end
end
endBest Practices and Common Pitfalls
Here are key best practices when working with multiple databases:
Plan Your Database Strategy: Carefully consider which data belongs in which database. Avoid splitting related data that needs to be queried together frequently.
Avoid Cross-Database Transactions: Rails doesn't support distributed transactions across databases. Design your application to handle this constraint.
Monitor Connection Pools: Each database connection consumes resources. Monitor and tune your connection pools appropriately.
Handle Replication Lag: When using read replicas, account for potential replication lag in your application logic.
Backup Strategy: Ensure your backup strategy covers all databases and maintains consistency across related data.
Testing Strategy: Test cross-database scenarios thoroughly, including failure cases where one database might be unavailable.
Conclusion
Multiple databases in Rails applications provide powerful solutions for scaling, performance optimization, and domain separation. While they introduce complexity, the benefits often outweigh the costs for growing applications.
The key to success is careful planning, proper configuration, and adherence to best practices. Start simple with clear separation of concerns, monitor performance closely, and be prepared to refactor as your application evolves.
Remember that multiple databases are a tool, not a requirement. Many successful Rails applications operate perfectly well with a single database. Consider your specific use case, performance requirements, and team expertise when making the decision to adopt multiple databases.
With Rails' robust multiple database support, you have the flexibility to design database architectures that grow with your application's needs while maintaining the developer productivity that makes Rails special.