Migrating from mysql2 to Trilogy Database Adapter in Rails
As Ruby on Rails applications evolve and scale, developers are always looking for ways to improve performance, memory efficiency, and reliability. One significant advancement in the Rails ecosystem is the introduction of the Trilogy database adapter as an alternative to the traditional mysql2 adapter. In this comprehensive guide, we’ll explore what Trilogy is, how to migrate from mysql2, and the benefits it brings to your Rails applications.
What is Trilogy?
Trilogy is a modern, high-performance MySQL-compatible database adapter for Ruby, developed by GitHub. It was designed from the ground up to address some of the limitations and performance bottlenecks found in the mysql2 gem. Trilogy aims to provide better memory efficiency, improved connection handling, and enhanced performance for Ruby applications that interact with MySQL databases.
Key Features of Trilogy
Native C Implementation: Trilogy is implemented in C, providing direct communication with MySQL servers without the overhead of additional abstraction layers.
Memory Efficiency: Optimized memory usage patterns that reduce garbage collection pressure and memory fragmentation.
Modern Protocol Support: Full support for modern MySQL protocol features and authentication methods.
Connection Pooling: Improved connection management and pooling mechanisms.
Error Handling: Better error reporting and handling compared to mysql2.
Understanding Trilogy with Examples
Let’s start with some basic examples to understand how Trilogy works in practice.
Basic Connection Example
require 'trilogy'
# Basic connection
client = Trilogy.new(
host: 'localhost',
port: 3306,
username: 'root',
password: 'password',
database: 'my_app_production'
)
# Execute a simple query
result = client.query("SELECT VERSION()")
puts result.to_a.first['VERSION()']
# Close the connection
client.close
Connection with SSL Configuration
require 'trilogy'
# SSL-enabled connection
client = Trilogy.new(
host: 'mysql.example.com',
port: 3306,
username: 'app_user',
password: 'secure_password',
database: 'production_db',
ssl_mode: :required,
ssl_ca: '/path/to/ca-cert.pem',
ssl_cert: '/path/to/client-cert.pem',
ssl_key: '/path/to/client-key.pem'
)
# Execute queries with SSL protection
users = client.query("SELECT id, email FROM users LIMIT 10")
users.each do |user|
puts "User: #{user['id']} - #{user['email']}"
end
client.close
Prepared Statements with Trilogy
require 'trilogy'
client = Trilogy.new(
host: 'localhost',
username: 'root',
password: 'password',
database: 'my_app'
)
# Prepare a statement
stmt = client.prepare("SELECT * FROM users WHERE age > ? AND city = ?")
# Execute with parameters
result = stmt.execute(25, 'San Francisco')
result.each do |row|
puts "#{row['name']} (#{row['age']}) from #{row['city']}"
end
# Clean up
stmt.close
client.close
Connection Pool Example
require 'trilogy'
require 'connection_pool'
# Create a connection pool
pool = ConnectionPool.new(size: 10, timeout: 5) do
Trilogy.new(
host: 'localhost',
username: 'app_user',
password: 'app_password',
database: 'production_db',
read_timeout: 10,
write_timeout: 10
)
end
# Use the pool
pool.with do |client|
result = client.query("SELECT COUNT(*) as count FROM orders WHERE created_at > NOW() - INTERVAL 1 DAY")
daily_orders = result.first['count']
puts "Orders in the last 24 hours: #{daily_orders}"
end
Rails Integration Examples
ActiveRecord Configuration
In your Rails application, you can configure Trilogy in your database.yml
:
# config/database.yml
production:
adapter: trilogy
encoding: utf8mb4
collation: utf8mb4_unicode_ci
database: my_app_production
username: app_user
password: <%= Rails.application.credentials.database[:password] %>
host: mysql.production.com
port: 3306
pool: 25
timeout: 5000
# Trilogy-specific configurations
read_timeout: 10
write_timeout: 10
connect_timeout: 5
# SSL configuration
ssl_mode: required
ssl_ca: /path/to/ca-cert.pem
Custom Trilogy Configuration Class
# config/initializers/trilogy.rb
class TrilogyConfiguration
def self.configure
ActiveRecord::Base.trilogy_adapter_class.class_eval do
# Custom configuration for Trilogy connections
def configure_connection
super
# Set custom timeouts
@connection.query_options[:read_timeout] = 30
@connection.query_options[:write_timeout] = 30
# Set session variables
execute("SET SESSION sql_mode = 'STRICT_TRANS_TABLES,NO_ZERO_DATE,NO_ZERO_IN_DATE,ERROR_FOR_DIVISION_BY_ZERO'")
execute("SET SESSION innodb_lock_wait_timeout = 10")
end
end
end
end
# Apply configuration after Rails initializes
Rails.application.config.after_initialize do
TrilogyConfiguration.configure
end
Step-by-Step Migration Guide
Step 1: Update Your Gemfile
First, you’ll need to add the trilogy gem and remove or comment out mysql2:
# Gemfile
# Comment out or remove the mysql2 gem
# gem 'mysql2', '~> 0.5'
# Add the trilogy gem
gem 'trilogy', '~> 2.4'
# If you're using Rails 7.1+, you might also need:
gem 'activerecord-trilogy-adapter', '~> 3.0'
Run bundle install:
bundle install
Step 2: Update Database Configuration
Update your config/database.yml
file to use the trilogy adapter:
# config/database.yml
default: &default
adapter: trilogy # Changed from mysql2
encoding: utf8mb4
pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
timeout: 5000
# Trilogy-specific timeouts
read_timeout: 10
write_timeout: 10
connect_timeout: 5
development:
<<: *default
database: my_app_development
username: root
password:
host: localhost
test:
<<: *default
database: my_app_test
username: root
password:
host: localhost
production:
<<: *default
database: my_app_production
username: <%= Rails.application.credentials.database[:username] %>
password: <%= Rails.application.credentials.database[:password] %>
host: <%= Rails.application.credentials.database[:host] %>
port: 3306
# Production-specific settings
pool: 25
ssl_mode: required
Step 3: Handle Custom mysql2 Code
If you have any direct mysql2-specific code in your application, you’ll need to update it. Here are common scenarios:
Raw Database Connections
Before (mysql2):
# Using mysql2 directly
require 'mysql2'
client = Mysql2::Client.new(
host: 'localhost',
username: 'root',
database: 'my_app'
)
result = client.query("SELECT * FROM users")
result.each do |row|
puts row['name']
end
After (Trilogy):
# Using Trilogy directly
require 'trilogy'
client = Trilogy.new(
host: 'localhost',
username: 'root',
database: 'my_app'
)
result = client.query("SELECT * FROM users")
result.each do |row|
puts row['name']
end
Custom Connection Handling
Before (mysql2):
class DatabaseService
def initialize
@client = Mysql2::Client.new(database_config)
end
def execute_query(sql)
@client.query(sql, symbolize_keys: true)
rescue Mysql2::Error => e
Rails.logger.error "Database error: #{e.message}"
raise
end
end
After (Trilogy):
class DatabaseService
def initialize
@client = Trilogy.new(database_config)
end
def execute_query(sql)
result = @client.query(sql)
# Convert to symbolized hash if needed
result.map(&:symbolize_keys)
rescue Trilogy::Error => e
Rails.logger.error "Database error: #{e.message}"
raise
end
end
Step 4: Update Connection Pool Configuration
If you’re using custom connection pooling, update it for Trilogy:
# config/initializers/database.rb
class CustomConnectionPool
def self.setup
config = Rails.application.config.database_configuration[Rails.env]
# Remove mysql2-specific options
trilogy_config = config.except('adapter').merge(
# Add Trilogy-specific options
read_timeout: config['read_timeout'] || 10,
write_timeout: config['write_timeout'] || 10,
connect_timeout: config['connect_timeout'] || 5
)
@pool = ConnectionPool.new(size: config['pool'] || 5) do
Trilogy.new(trilogy_config.symbolize_keys)
end
end
def self.with_connection(&block)
@pool.with(&block)
end
end
Step 5: Test the Migration
Before deploying to production, thoroughly test your application:
# Create a test script to verify the migration
# test/trilogy_migration_test.rb
require 'test_helper'
class TrilogyMigrationTest < ActiveSupport::TestCase
test "database connection works with trilogy" do
assert ActiveRecord::Base.connection.active?
assert_equal 'Trilogy', ActiveRecord::Base.connection.class.name
end
test "basic queries work" do
user_count = User.count
assert user_count >= 0
# Test complex query
recent_users = User.where("created_at > ?", 1.week.ago).limit(10)
assert recent_users.is_a?(ActiveRecord::Relation)
end
test "transactions work correctly" do
initial_count = User.count
User.transaction do
User.create!(name: "Test User", email: "test@example.com")
raise ActiveRecord::Rollback
end
assert_equal initial_count, User.count
end
test "prepared statements work" do
user = User.where(id: 1).first
assert_not_nil user if User.count > 0
end
end
Run your test suite:
# Run the specific migration test
rails test test/trilogy_migration_test.rb
# Run your full test suite
rails test
# Run system tests
rails test:system
Step 6: Monitor Performance
Create monitoring scripts to compare performance:
# scripts/performance_comparison.rb
require 'benchmark'
class PerformanceMonitor
def self.benchmark_queries
puts "Benchmarking database operations with Trilogy..."
Benchmark.bm(20) do |x|
x.report("Simple SELECT:") do
1000.times { User.first }
end
x.report("Complex JOIN:") do
100.times do
User.joins(:orders)
.where("orders.created_at > ?", 1.month.ago)
.limit(10)
.to_a
end
end
x.report("Bulk INSERT:") do
User.transaction do
100.times do |i|
User.create!(
name: "Bulk User #{i}",
email: "bulk#{i}@example.com"
)
end
end
end
x.report("Connection pool:") do
threads = []
10.times do
threads << Thread.new do
50.times { User.count }
end
end
threads.each(&:join)
end
end
end
end
# Run the benchmark
PerformanceMonitor.benchmark_queries
Deployment Considerations
Rolling Deployment Strategy
For production deployments, consider a rolling update approach:
# docker-compose.yml for blue-green deployment
version: '3.8'
services:
app-blue:
build: .
environment:
- DATABASE_ADAPTER=mysql2
- RAILS_ENV=production
depends_on:
- mysql
app-green:
build: .
environment:
- DATABASE_ADAPTER=trilogy
- RAILS_ENV=production
depends_on:
- mysql
mysql:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: rootpassword
MYSQL_DATABASE: app_production
Health Checks
Implement health checks for Trilogy connections:
# app/controllers/health_controller.rb
class HealthController < ApplicationController
def database
start_time = Time.current
# Test basic connectivity
ActiveRecord::Base.connection.execute("SELECT 1")
# Test query performance
User.limit(1).to_a
response_time = (Time.current - start_time) * 1000
render json: {
status: 'ok',
adapter: ActiveRecord::Base.connection.adapter_name,
response_time_ms: response_time.round(2),
pool_size: ActiveRecord::Base.connection_pool.size,
active_connections: ActiveRecord::Base.connection_pool.connections.count
}
rescue => e
render json: {
status: 'error',
error: e.message,
adapter: 'unknown'
}, status: 503
end
end
Benefits of Trilogy over mysql2
1. Enhanced Performance
Memory Efficiency: Trilogy uses significantly less memory than mysql2, especially for applications with high connection counts or large result sets.
# Performance comparison example
require 'benchmark'
require 'memory_profiler'
def memory_usage_comparison
# Before migration (conceptual - you'd run this with mysql2)
mysql2_report = MemoryProfiler.report do
1000.times { User.limit(100).to_a }
end
# After migration with Trilogy
trilogy_report = MemoryProfiler.report do
1000.times { User.limit(100).to_a }
end
puts "mysql2 total allocated: #{mysql2_report.total_allocated_memsize}"
puts "Trilogy total allocated: #{trilogy_report.total_allocated_memsize}"
puts "Memory improvement: #{((mysql2_report.total_allocated_memsize - trilogy_report.total_allocated_memsize) / mysql2_report.total_allocated_memsize.to_f * 100).round(2)}%"
end
Faster Query Execution: Optimized C implementation provides faster query execution times.
# Benchmark query performance
Benchmark.bm(15) do |x|
x.report("Large result set:") do
Product.joins(:category).limit(10000).to_a
end
x.report("Complex aggregation:") do
Order.group(:status)
.joins(:user)
.where("created_at > ?", 30.days.ago)
.count
end
end
2. Better Connection Management
Improved Connection Pooling: Trilogy provides more efficient connection pooling with better resource cleanup.
# Monitor connection pool efficiency
class ConnectionPoolMonitor
def self.stats
pool = ActiveRecord::Base.connection_pool
{
size: pool.size,
checked_out: pool.checked_out_connections,
available: pool.available_connection_count,
queue_length: pool.num_waiting_in_queue,
reaper_frequency: pool.reaper&.frequency
}
end
def self.log_stats
stats = self.stats
Rails.logger.info "Connection Pool Stats: #{stats}"
stats
end
end
# Use in a background job or monitoring system
ConnectionPoolMonitor.log_stats
Connection Timeout Handling: More robust timeout handling prevents connection leaks.
# config/application.rb
config.after_initialize do
ActiveRecord::Base.connection_pool.with_connection do |conn|
# Trilogy provides better timeout configuration
conn.execute("SET SESSION wait_timeout = 28800")
conn.execute("SET SESSION interactive_timeout = 28800")
end
end
3. Enhanced Security Features
Better SSL/TLS Support: Improved SSL configuration and certificate validation.
# Enhanced SSL configuration with Trilogy
production:
adapter: trilogy
ssl_mode: verify_identity # Stronger than mysql2's ssl_mode
ssl_ca: /path/to/ca-cert.pem
ssl_cert: /path/to/client-cert.pem
ssl_key: /path/to/client-key.pem
ssl_cipher: 'ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256'
Modern Authentication: Support for latest MySQL authentication plugins.
# Modern authentication support
client = Trilogy.new(
host: 'mysql8-server.com',
username: 'app_user',
password: 'secure_password',
database: 'production_db',
auth_plugin: 'caching_sha2_password' # MySQL 8.0 default
)
4. Improved Error Handling and Debugging
Detailed Error Messages: Better error reporting for debugging database issues.
# Enhanced error handling with Trilogy
class DatabaseErrorHandler
def self.handle_trilogy_errors
yield
rescue Trilogy::ConnectionError => e
Rails.logger.error "Connection failed: #{e.message}"
# Trilogy provides more specific error types
case e.error_code
when 2003
Rails.logger.error "Cannot connect to MySQL server"
when 1045
Rails.logger.error "Access denied for user"
when 1049
Rails.logger.error "Unknown database"
end
raise
rescue Trilogy::QueryError => e
Rails.logger.error "Query failed: #{e.message}"
Rails.logger.error "SQL State: #{e.sql_state}"
raise
end
end
Better Monitoring Capabilities: Enhanced metrics and monitoring support.
# Advanced monitoring with Trilogy
class TrilogyMonitor
def self.connection_metrics
ActiveRecord::Base.connection_pool.with_connection do |conn|
{
server_version: conn.server_version,
client_version: Trilogy::VERSION,
connection_id: conn.thread_id,
ssl_enabled: conn.ssl_cipher.present?,
charset: conn.charset,
timezone: conn.query("SELECT @@session.time_zone").first.values.first
}
end
end
def self.query_performance_stats
# Trilogy provides better introspection
stats = ActiveRecord::Base.connection.query_cache_stats
{
query_cache_enabled: ActiveRecord::Base.connection.query_cache_enabled,
query_cache_size: stats[:size],
cache_hits: stats[:hits],
cache_misses: stats[:misses]
}
end
end
5. Long-term Sustainability
Active Development: Trilogy is actively maintained by GitHub with regular updates and improvements.
Modern Codebase: Built with modern C practices and memory management techniques.
Community Support: Growing community adoption and contribution.
# Check Trilogy version and features
puts "Trilogy version: #{Trilogy::VERSION}"
puts "Supported features: #{Trilogy.features}"
# Performance monitoring for long-term tracking
class PerformanceTracker
def self.track_migration_benefits
metrics = {
memory_usage: `ps -o rss= -p #{Process.pid}`.strip.to_i,
connection_count: ActiveRecord::Base.connection_pool.connections.size,
query_response_time: benchmark_query_time,
error_rate: calculate_error_rate
}
# Store metrics for trend analysis
Rails.cache.write("trilogy_metrics_#{Time.current.to_date}", metrics)
metrics
end
private
def self.benchmark_query_time
start_time = Time.current
User.limit(100).to_a
(Time.current - start_time) * 1000
end
def self.calculate_error_rate
# Implementation depends on your error tracking system
0.0
end
end
Conclusion
Migrating from mysql2 to Trilogy represents a significant step forward for Rails applications that rely on MySQL databases. The migration process, while requiring careful planning and testing, offers substantial benefits in terms of performance, memory efficiency, and long-term maintainability.
The key advantages of Trilogy include:
- Reduced memory footprint and better garbage collection behavior
- Improved connection pooling and resource management
- Enhanced security features with modern SSL/TLS support
- Better error handling and debugging capabilities
- Active development backed by GitHub’s expertise
When planning your migration, remember to:
- Thoroughly test in development and staging environments
- Update any direct mysql2 dependencies in your codebase
- Monitor performance metrics before and after migration
- Plan for a gradual rollout in production environments
- Update your monitoring and alerting systems to work with Trilogy
As the Rails ecosystem continues to evolve, Trilogy represents the future of MySQL connectivity for Ruby applications. By migrating now, you’re positioning your application for better performance and long-term sustainability.
Whether you’re building a new application or maintaining an existing one, Trilogy offers a compelling upgrade path that delivers immediate performance benefits while future-proofing your database connectivity layer.