连接管理器

连接管理器

本指南涵盖 db.manager,即跟踪每个已注册数据库连接的连接管理器。你将学会如何:

  • 检查哪些连接已注册以及哪些当前已打开
  • 打开、关闭和释放命名连接
  • 在运行时为多租户工作流注册新连接
  • 替换现有连接的配置而不中断正在执行的查询
  • 订阅连接生命周期事件
  • 获取底层 knex 实例及其连接池

概述

当应用启动时,Lucid 读取 config/database.ts 中定义的每个连接,并将其注册到管理器。注册会记录配置,但不会打开连接池。连接池在首次查询时惰性创建,并在进程的剩余生命周期中重用。AdonisJS 在应用关闭期间会关闭每个打开的连接池。

应用代码很少直接访问 db.manager。你需要使用它的三种情况是:运行时连接管理(例如,每个租户独立的数据库)、需要在进程退出前关闭连接池的脚本和 Ace 命令,以及记录或报告连接事件的可观测性代码。

访问管理器

导入 db 服务并读取 manager 属性。

app/services/tenant_service.ts
import db from '@adonisjs/lucid/services/db'
export default class TenantService {
listRegistered() {
return Array.from(db.manager.connections.keys())
}
}

db.manager.connections 是一个 Map<string, ConnectionNode>。每个节点携带连接的 name、其 config、当前 state,以及连接池打开后的活跃 connection 实例。state 字段是 registeredopenmigratingclosingclosed 之一,描述连接在其生命周期中的位置。

检查注册和连接状态

使用 has(name) 检查某个连接名称是否已在管理器中注册,使用 isConnected(name) 检查其连接池是否已实际打开。连接可以在未连接的情况下被注册,因为注册在启动时发生,而连接池在首次使用时才打开。

app/services/diagnostics_service.ts
import db from '@adonisjs/lucid/services/db'
export default class DiagnosticsService {
status(name: string) {
return {
registered: db.manager.has(name),
connected: db.manager.isConnected(name),
}
}
}

使用 get(name) 读取完整的 ConnectionNode,例如当你需要检查当前 state 或连接注册时的 config 时。

const node = db.manager.get('primary')
if (node) {
logger.info({ state: node.state, client: node.config.client })
}

主动打开连接

连接池是惰性创建的,因此 connect(name) 很少需要。在数据库不可达时应快速失败的脚本中,或在期望连接池在轮询之前已存在的健康探测中,使用它。

commands/warm_pools.ts
import { BaseCommand } from '@adonisjs/core/ace'
import db from '@adonisjs/lucid/services/db'
export default class WarmPools extends BaseCommand {
static commandName = 'pools:warm'
async run() {
db.manager.connect('primary')
db.manager.connect('analytics')
}
}

当连接已打开时,connect 是空操作;当名称未通过 config/database.tsadd 注册时,抛出 E_UNMANAGED_DB_CONNECTION

关闭和释放连接

close(name) 断开单个连接的连接池,closeAll() 一次性断开所有连接池。两者都接受一个可选的 release 标志,该标志还会从管理器中移除该条目,因此名称必须通过 add 重新注册才能再次使用。

commands/reports.ts
import { BaseCommand } from '@adonisjs/core/ace'
import db from '@adonisjs/lucid/services/db'
export default class Reports extends BaseCommand {
static commandName = 'reports:run'
async run() {
try {
await db.connection('analytics').from('daily_reports').select('*')
} finally {
await db.manager.closeAll()
}
}
}

调用 release(name) 从管理器中完全移除连接。当连接已打开时,release 会先关闭它,然后再移除条目。

不要从普通的 HTTP 请求处理程序中关闭连接。同一进程中后续的代码可能会尝试重用连接池并失败。仅在拥有进程生命周期的脚本、worker、测试和关闭流程中关闭连接。

参见在脚本中管理连接部分,了解在上下文中使用相同模式的详细说明。

在运行时注册连接

使用 add(name, config) 在应用启动后注册连接。这是多租户工作流的入口点,每个租户对应一个独立的数据库。

app/services/tenant_connections.ts
import db from '@adonisjs/lucid/services/db'
import type { ConnectionConfig } from '@adonisjs/lucid/types/database'
export default class TenantConnections {
register(tenantId: string, config: ConnectionConfig) {
const name = `tenant:${tenantId}`
if (!db.manager.has(name)) {
db.manager.add(name, config)
}
return db.connection(name)
}
async forget(tenantId: string) {
await db.manager.release(`tenant:${tenantId}`)
}
}

config 参数与 config/database.ts 中的条目具有相同的形状。参见 Knex 配置选项了解底层驱动支持的选项列表。

当名称已注册时,add 是空操作,因此它不会覆盖现有配置。使用 patch 更改已注册连接的配置,或先 release 旧的再 add 新的。

替换连接的配置

patch(name, config) 更新已注册连接的配置。当连接当前已打开时,活跃的连接池被移动到内部孤立集合并在后台断开连接,这让已在执行中的查询能够干净地完成。下一次调用 connect 或通过 db.connection(name) 执行的下一个查询将使用新配置打开一个全新的连接池。

import db from '@adonisjs/lucid/services/db'
db.manager.patch('tenant:acme', {
client: 'pg',
connection: { host: 'db-new.internal', database: 'acme' },
})

对尚未注册的名称调用 patch 等同于 add

订阅生命周期事件

管理器在 AdonisJS 事件发射器上发出三个事件。订阅它们以记录连接变动或将其转发到可观测系统。

start/events.ts
import emitter from '@adonisjs/core/services/emitter'
import logger from '@adonisjs/core/services/logger'
emitter.on('db:connection:connect', (connection) => {
logger.info({ connection: connection.name }, 'db connection opened')
})
emitter.on('db:connection:disconnect', (connection) => {
logger.info({ connection: connection.name }, 'db connection closed')
})
emitter.on('db:connection:error', ([error, connection]) => {
logger.error({ err: error, connection: connection.name }, 'db connection error')
})

db:connection:error 的有效载荷是 [error, connection] 元组,而其他两个事件直接接收连接。

获取底层 Connection

节点上的 connection 属性是 Lucid 的 Connection 实例,是 knex 客户端及其 tarn 连接池的薄包装。通过 db.manager.get(name)?.connection 访问它。

app/services/pool_inspector.ts
import db from '@adonisjs/lucid/services/db'
export default class PoolInspector {
usedConnections(name: string) {
const connection = db.manager.get(name)?.connection
if (!connection?.pool) {
return 0
}
return connection.pool.numUsed()
}
}

Connection 实例暴露 clientreadClient 作为写和读副本的底层 knex 实例;当未配置副本时,readClientclient 是同一个实例。poolreadPool getter 返回这些客户端背后的 tarn 连接池,两者在连接打开之前都是 nullnameconfigclientNamehasReadWriteReplicas 是只读元数据,ready 在至少存在一个 knex 客户端时变为 truedialectNameclientName 的已弃用别名,仅为向后兼容而保留。

应用代码应优先使用 db.connection(name),它返回一个具有完整构建器、事务和副本感知模式选择的查询客户端。仅在需要检查连接池、调用 Lucid 未暴露的 knex API 或围绕底层客户端构建工具时,才降级到原始 Connection

后续步骤