Connection Management
Adonis ODM provides robust connection management features for MongoDB, including connection pooling, multiple database support, and connection health monitoring. This guide covers everything you need to know about managing database connections effectively.
Connection Configuration
Basic Connection Setup
// config/odm.ts
import env from '#start/env'
import { defineConfig } from 'adonis-odm'
const odmConfig = defineConfig({
connection: 'mongodb',
connections: {
mongodb: {
client: 'mongodb',
connection: {
host: env.get('MONGO_HOST', 'localhost'),
port: env.get('MONGO_PORT', 27017),
database: env.get('MONGO_DATABASE'),
username: env.get('MONGO_USERNAME'),
password: env.get('MONGO_PASSWORD'),
},
pool: {
min: 2,
max: 10,
acquireTimeoutMillis: 30000,
createTimeoutMillis: 30000,
destroyTimeoutMillis: 5000,
idleTimeoutMillis: 30000,
reapIntervalMillis: 1000,
createRetryIntervalMillis: 200,
}
}
}
})
export default odmConfig
Connection String Configuration
// Using MongoDB connection string
const odmConfig = defineConfig({
connection: 'mongodb',
connections: {
mongodb: {
client: 'mongodb',
connection: {
connectionString: env.get('MONGO_URI', 'mongodb://localhost:27017/myapp')
},
pool: {
min: 2,
max: 20
}
}
}
})
Advanced Connection Options
const odmConfig = defineConfig({
connections: {
mongodb: {
client: 'mongodb',
connection: {
host: env.get('MONGO_HOST'),
port: env.get('MONGO_PORT'),
database: env.get('MONGO_DATABASE'),
username: env.get('MONGO_USERNAME'),
password: env.get('MONGO_PASSWORD'),
// SSL/TLS options
ssl: true,
sslValidate: true,
sslCA: env.get('MONGO_SSL_CA'),
sslCert: env.get('MONGO_SSL_CERT'),
sslKey: env.get('MONGO_SSL_KEY'),
// Authentication options
authSource: 'admin',
authMechanism: 'SCRAM-SHA-256',
// Connection options
connectTimeoutMS: 30000,
socketTimeoutMS: 30000,
serverSelectionTimeoutMS: 30000,
maxPoolSize: 10,
minPoolSize: 2,
maxIdleTimeMS: 30000,
// Replica set options
replicaSet: 'myReplicaSet',
readPreference: 'primaryPreferred',
// Write concern
w: 'majority',
j: true,
wtimeoutMS: 5000
}
}
}
})
Multiple Database Connections
Defining Multiple Connections
const odmConfig = defineConfig({
connection: 'primary',
connections: {
primary: {
client: 'mongodb',
connection: {
host: 'primary-db.example.com',
database: 'main_app'
}
},
analytics: {
client: 'mongodb',
connection: {
host: 'analytics-db.example.com',
database: 'analytics'
}
},
logs: {
client: 'mongodb',
connection: {
host: 'logs-db.example.com',
database: 'application_logs'
}
}
}
})
Using Different Connections in Models
// Primary database model
export default class User extends BaseModel {
static connection = 'primary' // Uses primary connection
@column({ isPrimary: true })
declare _id: string
@column()
declare name: string
}
// Analytics database model
export default class UserAnalytics extends BaseModel {
static connection = 'analytics' // Uses analytics connection
@column({ isPrimary: true })
declare _id: string
@column()
declare userId: string
@column()
declare pageViews: number
}
// Logs database model
export default class ApplicationLog extends BaseModel {
static connection = 'logs' // Uses logs connection
@column({ isPrimary: true })
declare _id: string
@column()
declare level: string
@column()
declare message: string
}
Runtime Connection Selection
// Query specific connection
const users = await User.query()
.connection('primary')
.all()
// Use different connection for specific operation
const analytics = await UserAnalytics.query()
.connection('analytics')
.where('userId', userId)
.all()
// Transaction on specific connection
await Database.transaction(async (trx) => {
const user = await User.create(userData, { client: trx })
const log = await ApplicationLog.create(logData, { client: trx })
}, { connection: 'primary' })
Connection Pooling
Pool Configuration
const odmConfig = defineConfig({
connections: {
mongodb: {
connection: {
// Connection details
},
pool: {
// Minimum connections to maintain
min: 2,
// Maximum connections in pool
max: 20,
// Time to wait for connection (ms)
acquireTimeoutMillis: 30000,
// Time to wait for connection creation (ms)
createTimeoutMillis: 30000,
// Time to wait for connection destruction (ms)
destroyTimeoutMillis: 5000,
// Time before idle connection is destroyed (ms)
idleTimeoutMillis: 30000,
// How often to check for idle connections (ms)
reapIntervalMillis: 1000,
// Interval between connection creation retries (ms)
createRetryIntervalMillis: 200,
// Validate connections before use
testOnBorrow: true,
// Validate connections when returned
testOnReturn: false
}
}
}
})
Pool Monitoring
import Database from '@ioc:Adonis/Lucid/Database'
// Get pool statistics
const poolStats = Database.connection('mongodb').pool.stats()
console.log('Pool stats:', {
size: poolStats.size,
available: poolStats.available,
borrowed: poolStats.borrowed,
invalid: poolStats.invalid,
pending: poolStats.pending
})
// Monitor pool events
Database.connection('mongodb').pool.on('createRequest', () => {
console.log('Connection creation requested')
})
Database.connection('mongodb').pool.on('createSuccess', () => {
console.log('Connection created successfully')
})
Database.connection('mongodb').pool.on('createFail', (error) => {
console.error('Connection creation failed:', error)
})
Database.connection('mongodb').pool.on('destroySuccess', () => {
console.log('Connection destroyed')
})
Connection Health Monitoring
Health Check Implementation
import Database from '@ioc:Adonis/Lucid/Database'
class DatabaseHealthCheck {
public async check() {
const connections = ['primary', 'analytics', 'logs']
const results = {}
for (const connectionName of connections) {
try {
const connection = Database.connection(connectionName)
// Test connection with ping
await connection.rawQuery({ ping: 1 })
// Get connection stats
const stats = connection.pool.stats()
results[connectionName] = {
status: 'healthy',
pool: {
size: stats.size,
available: stats.available,
borrowed: stats.borrowed
}
}
} catch (error) {
results[connectionName] = {
status: 'unhealthy',
error: error.message
}
}
}
return results
}
public async checkConnection(connectionName: string) {
try {
const connection = Database.connection(connectionName)
// Ping database
const start = Date.now()
await connection.rawQuery({ ping: 1 })
const responseTime = Date.now() - start
return {
status: 'healthy',
responseTime,
pool: connection.pool.stats()
}
} catch (error) {
return {
status: 'unhealthy',
error: error.message
}
}
}
}
// Usage in health check endpoint
export default class HealthController {
public async database({ response }: HttpContextContract) {
const healthCheck = new DatabaseHealthCheck()
const results = await healthCheck.check()
const allHealthy = Object.values(results).every(
result => result.status === 'healthy'
)
return response.status(allHealthy ? 200 : 503).json({
status: allHealthy ? 'healthy' : 'unhealthy',
connections: results
})
}
}
Automatic Reconnection
// Connection with retry logic
class ConnectionManager {
private maxRetries = 3
private retryDelay = 1000
public async withRetry<T>(
operation: () => Promise<T>,
connectionName: string = 'mongodb'
): Promise<T> {
let lastError: Error
for (let attempt = 1; attempt <= this.maxRetries; attempt++) {
try {
return await operation()
} catch (error) {
lastError = error
if (this.isConnectionError(error) && attempt < this.maxRetries) {
console.log(`Connection attempt ${attempt} failed, retrying...`)
// Wait before retry
await this.delay(this.retryDelay * attempt)
// Try to reconnect
await this.reconnect(connectionName)
continue
}
throw error
}
}
throw lastError!
}
private isConnectionError(error: any): boolean {
return error.name === 'MongoNetworkError' ||
error.name === 'MongoServerSelectionError' ||
error.code === 'ECONNREFUSED'
}
private async reconnect(connectionName: string) {
try {
const connection = Database.connection(connectionName)
await connection.disconnect()
await connection.connect()
} catch (error) {
console.error('Reconnection failed:', error)
}
}
private delay(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms))
}
}
Connection Lifecycle Management
Application Startup
// start/database.ts
import Database from '@ioc:Adonis/Lucid/Database'
export default class DatabaseProvider {
public async ready() {
// Test all connections on startup
const connections = ['primary', 'analytics', 'logs']
for (const connectionName of connections) {
try {
await Database.connection(connectionName).rawQuery({ ping: 1 })
console.log(`✅ ${connectionName} connection established`)
} catch (error) {
console.error(`❌ ${connectionName} connection failed:`, error.message)
throw error
}
}
}
public async shutdown() {
// Gracefully close all connections
const connections = ['primary', 'analytics', 'logs']
for (const connectionName of connections) {
try {
await Database.connection(connectionName).disconnect()
console.log(`🔌 ${connectionName} connection closed`)
} catch (error) {
console.error(`Error closing ${connectionName}:`, error.message)
}
}
}
}
Graceful Shutdown
// Handle graceful shutdown
process.on('SIGTERM', async () => {
console.log('Received SIGTERM, shutting down gracefully...')
try {
// Close all database connections
await Database.manager.closeAll()
console.log('All database connections closed')
process.exit(0)
} catch (error) {
console.error('Error during shutdown:', error)
process.exit(1)
}
})
process.on('SIGINT', async () => {
console.log('Received SIGINT, shutting down gracefully...')
try {
await Database.manager.closeAll()
process.exit(0)
} catch (error) {
console.error('Error during shutdown:', error)
process.exit(1)
}
})
Performance Optimization
Connection Tuning
// Production-optimized configuration
const odmConfig = defineConfig({
connections: {
mongodb: {
connection: {
// Optimize for production
maxPoolSize: 50,
minPoolSize: 5,
maxIdleTimeMS: 30000,
serverSelectionTimeoutMS: 5000,
socketTimeoutMS: 45000,
// Read preference for scaling
readPreference: 'secondaryPreferred',
// Write concern for performance
w: 1,
j: false,
wtimeoutMS: 5000
},
pool: {
min: 5,
max: 50,
acquireTimeoutMillis: 10000,
idleTimeoutMillis: 30000
}
}
}
})
Connection Monitoring
// Monitor connection performance
class ConnectionMonitor {
private metrics = new Map()
public startMonitoring() {
setInterval(() => {
this.collectMetrics()
}, 30000) // Every 30 seconds
}
private async collectMetrics() {
const connections = ['primary', 'analytics', 'logs']
for (const connectionName of connections) {
try {
const connection = Database.connection(connectionName)
const stats = connection.pool.stats()
this.metrics.set(connectionName, {
timestamp: new Date(),
poolSize: stats.size,
available: stats.available,
borrowed: stats.borrowed,
pending: stats.pending
})
// Log warnings for pool exhaustion
if (stats.available === 0 && stats.pending > 0) {
console.warn(`⚠️ Connection pool exhausted for ${connectionName}`)
}
} catch (error) {
console.error(`Error collecting metrics for ${connectionName}:`, error)
}
}
}
public getMetrics(connectionName?: string) {
if (connectionName) {
return this.metrics.get(connectionName)
}
return Object.fromEntries(this.metrics)
}
}
Testing Connections
Connection Testing
import { test } from '@japa/runner'
import Database from '@ioc:Adonis/Lucid/Database'
test.group('Database connections', () => {
test('should connect to primary database', async ({ assert }) => {
const result = await Database.connection('primary').rawQuery({ ping: 1 })
assert.equal(result.ok, 1)
})
test('should handle connection failures gracefully', async ({ assert }) => {
// Test with invalid connection
await assert.rejects(async () => {
await Database.connection('invalid').rawQuery({ ping: 1 })
})
})
test('should maintain connection pool', async ({ assert }) => {
const connection = Database.connection('primary')
const stats = connection.pool.stats()
assert.isTrue(stats.size >= 2) // Minimum pool size
assert.isTrue(stats.size <= 20) // Maximum pool size
})
})
Next Steps
- Pagination - Efficiently paginate large datasets
- Model Lifecycle - Understand connection hooks
- Database Manager API - Complete API reference
- Configuration - Advanced configuration options