数据库服务
本指南涵盖 db 服务。你将学会如何:
- 对表和模型创建查询
- 在运行时选择连接和读/写模式
- 安全地运行原始 SQL
- 清空表并与应用锁协调
- 在脚本和 worker 中管理连接生命周期
概述
db 服务是 Lucid 数据库操作的入口点。它本身不是查询构建器。相反,它返回查询客户端,这些客户端创建 select、insert 和 raw 查询构建器指南中所述的构建器。
当你想要直接查询表、运行原始 SQL、启动事务、在运行时选择连接或编写不经过模型的代码时,使用 db 服务。
导入服务
从 @adonisjs/lucid/services/db 导入服务。
import type { HttpContext } from '@adonisjs/core/http'
import db from '@adonisjs/lucid/services/db'
export default class PostsController {
async index({ response }: HttpContext) {
const posts = await db.from('posts').select('id', 'title')
return response.ok(posts)
}
}
查询入口点
db 服务暴露了四个创建查询构建器的入口点。from 和 table 是你最常用的快捷方式,query 和 insertQuery 适用于你想要设置类型参数或选择模式而不指定连接名称的情况。
import type { HttpContext } from '@adonisjs/core/http'
import db from '@adonisjs/lucid/services/db'
export default class PostsController {
async index() {
return db
.from('posts')
.select('id', 'title')
.orderBy('id', 'desc')
}
async store({ request, response }: HttpContext) {
const [post] = await db
.table('posts')
.insert({ title: request.input('title') })
.returning(['id', 'title'])
return response.created(post)
}
}
当你想要一个没有预选表的构建器时,使用 db.query() 或 db.insertQuery()。这对于显式指定结果形状类型,或在不通过 db.connection() 的情况下选择连接模式很有用。
import db from '@adonisjs/lucid/services/db'
export default class PostsService {
async listPublished() {
return db
.query<{ id: number; title: string }>({ mode: 'read' })
.from('posts')
.where('is_published', true)
.select('id', 'title')
}
}
运行原始 SQL
当整个 SQL 语句是原始的并应执行时,使用 db.rawQuery。将动态值作为第二个参数绑定,而不是将其插值到 SQL 字符串中。
import type { HttpContext } from '@adonisjs/core/http'
import db from '@adonisjs/lucid/services/db'
export default class PostsController {
async showByTitle({ params, response }: HttpContext) {
const result = await db.rawQuery(
'select * from posts where title = ?',
[params.title]
)
return response.ok(result.rows)
}
}
使用 db.raw 将原始片段嵌入另一个查询构建器,当值是列引用而非用户提供的数据时,使用 db.ref。
import type { HttpContext } from '@adonisjs/core/http'
import db from '@adonisjs/lucid/services/db'
export default class PostsController {
async index({ response }: HttpContext) {
const posts = await db
.from('posts')
.select('posts.id', 'posts.title')
.select(
db.raw(
'(select count(*) from comments where comments.post_id = posts.id) as comments_count'
)
)
.orderBy(db.ref('posts.created_at'), 'desc')
return response.ok(posts)
}
}
切勿通过将用户输入拼接到查询字符串中来构建原始 SQL,因为这可能导致 SQL 注入漏洞。
将用户输入作为绑定传入,如 rawQuery('... where title = ?', [title]) 所示。
启动事务
当多个数据库操作必须一起成功或一起失败时,使用 db.transaction。托管事务在回调成功时提交,在回调抛出时回滚。
import type { HttpContext } from '@adonisjs/core/http'
import db from '@adonisjs/lucid/services/db'
export default class UsersController {
async store({ request, response }: HttpContext) {
const user = await db.transaction(async (trx) => {
const [createdUser] = await trx
.table('users')
.insert({ email: request.input('email') })
.returning(['id', 'email'])
await trx
.table('profiles')
.insert({
user_id: createdUser.id,
full_name: request.input('full_name'),
})
return createdUser
})
return response.created(user)
}
}
参见事务指南了解手动事务、隔离级别、保存点以及将事务与模型一起使用。
选择连接
调用 db.connection 查询默认连接以外的连接。返回的查询客户端暴露与 db 服务本身相同的入口点,因此你可以从那里链式调用 .from、.table、.query 或 .rawQuery。
import type { HttpContext } from '@adonisjs/core/http'
import db from '@adonisjs/lucid/services/db'
export default class AnalyticsController {
async signups({ response }: HttpContext) {
const signups = await db
.connection('analytics')
.from('daily_signups')
.select('date', 'total')
.orderBy('date', 'desc')
return response.ok(signups)
}
}
当连接配置了读/写副本时,传入 mode 选项来选择特定的副本集。对可以发送到读副本的查询使用 read,对必须命中主节点的写操作或写后读工作流使用 write。
import type { HttpContext } from '@adonisjs/core/http'
import db from '@adonisjs/lucid/services/db'
export default class PostsController {
async index() {
return db
.connection('postgres', { mode: 'read' })
.from('posts')
.where('is_published', true)
}
async publish({ params, response }: HttpContext) {
await db
.connection('postgres', { mode: 'write' })
.from('posts')
.where('id', params.id)
.update({ is_published: true })
return response.noContent()
}
}
省略 options 对象以获取双模式客户端,它将读操作自动路由到读副本,将写操作自动路由到主节点。
动态查询模型
大多数模型查询应直接使用 User.query() 或另一个模型的静态查询方法。当基础设施代码动态接收模型类并需要构建针对它的查询时,使用 db.modelQuery。
import db from '@adonisjs/lucid/services/db'
import type { LucidModel } from '@adonisjs/lucid/types/model'
export default class ModelSearchService {
search(model: LucidModel, term: string) {
return db
.modelQuery(model)
.whereLike('title', `%${term}%`)
.limit(20)
}
}
对于普通的应用代码,优先使用 Model.query()。
清空表
每个查询客户端都暴露了 truncate 和 truncateAllTables 助手。这些在测试设置和拆卸中最有用,或在需要从干净状态开始的种子脚本中。
import db from '@adonisjs/lucid/services/db'
export async function resetDatabase() {
await db.connection().truncateAllTables(['adonis_schema', 'adonis_schema_versions'])
}
将表名传给 truncate 来清空单个表。可选的第二个参数在支持级联清空的数据库上启用级联。
await db.connection().truncate('posts')
await db.connection('postgres').truncate('comments', true)
应用锁
应用锁在不创建行锁的情况下协调跨进程的工作。一个常见的用例是去重计划作业:只有第一个获取锁的进程才运行工作。
import db from '@adonisjs/lucid/services/db'
export async function runDailyReport() {
const client = db.connection()
const acquired = await client.getAdvisoryLock('daily-report')
if (!acquired) {
return
}
try {
// 在所有进程中只运行一次工作
} finally {
await client.releaseAdvisoryLock('daily-report')
}
}
应用锁在 PostgreSQL 和 MySQL 上受支持。在 SQLite、MSSQL、Oracle 或 Redshift 上调用 getAdvisoryLock 会抛出错误。
在脚本中管理连接
Lucid 在应用启动时注册每个已配置的连接,但它惰性打开底层数据库连接。对连接的第一个查询创建它,后续查询通过连接池重用它。AdonisJS 在应用关闭期间自动关闭每个打开的连接。
对于普通的 HTTP 和作业负载,你永远不需要手动操作连接生命周期。脚本、worker 和一次性命令是例外:它们拥有一个在工作完成后退出的进程,因此有时需要显式关闭连接。
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()
}
}
}
db.manager.close(name) 关闭特定连接,db.manager.closeAll() 一次性关闭所有打开的连接。当诊断命令或健康检查需要知道命名连接是否已打开时,调用 db.manager.isConnected(name)。
参见连接管理器指南了解完整的 db.manager API,包括运行时注册、配置修补和生命周期事件。
不要从普通的 HTTP 请求处理程序中关闭连接。同一进程中后续的代码可能会尝试重用连接并失败。
仅在拥有完整生命周期的独立脚本、测试、worker 或应用关闭流程中关闭连接。
调试单个查询
db 服务创建的每个查询构建器都可以为单个查询启用调试输出。当你想检查一个查询而不在连接级别启用调试模式时,这很有用。
import db from '@adonisjs/lucid/services/db'
export default class PostsController {
async index() {
return db
.from('posts')
.where('is_published', true)
.debug(true)
}
}
Lucid 为每个执行的查询发出 db:query 事件。当你想要记录查询或将其转发到可观测系统时,订阅它。
import emitter from '@adonisjs/core/services/emitter'
import logger from '@adonisjs/core/services/logger'
emitter.on('db:query', (query) => {
logger.debug(query)
})
参见调试指南了解连接级调试模式和格式化打印的 SQL 输出。
使用健康检查
Lucid 导出了两个与 AdonisJS 健康检查系统集成的健康检查。DbCheck 验证连接是否可以执行简单查询,DbConnectionCountCheck 报告支持的方言上的活跃连接数。
在 start/health.ts 中将检查与其他 AdonisJS 检查一起注册。
import db from '@adonisjs/lucid/services/db'
import { DbCheck, DbConnectionCountCheck } from '@adonisjs/lucid/database'
import { HealthChecks, DiskSpaceCheck, MemoryHeapCheck } from '@adonisjs/core/health'
export const healthChecks = new HealthChecks().register([
new DiskSpaceCheck().cacheFor('1 hour'),
new MemoryHeapCheck(),
new DbCheck(db.connection()),
new DbConnectionCountCheck(db.connection())
.warnWhenExceeds(20)
.failWhenExceeds(30),
])
参见 AdonisJS 健康检查指南了解将已注册的检查连接到端点。
降级到 Knex
Lucid 的查询构建器包装了 Knex 但未暴露每个 Knex 方法。当你需要 Lucid 未暴露的 API(例如 Knex 的 rowNumber 或 rank 分析助手)时,使用 db.knexQuery() 或 db.knexRawQuery() 直接获取底层 Knex 构建器。
import db from '@adonisjs/lucid/services/db'
const rankings = await db
.knexQuery()
.from('scores')
.select('user_id', 'game_id', 'points')
.rowNumber('rank', { column: 'points', order: 'desc' }, 'game_id')
结果形状来自 Knex 而非 Lucid 的带类型构建器,因此你将失去该查询的 Lucid 结果类型。谨慎使用此逃生舱口,对于在 Lucid 构建的查询中的一次性 SQL 片段,优先使用 db.raw。
后续步骤
- Select 查询构建器指南了解 select、update、delete、分页和条件查询 API。
- Insert 查询构建器指南了解 insert 和 upsert API。
- Raw 查询构建器指南了解原始 SQL 片段和绑定。
- 事务指南了解托管事务、隔离级别和保存点。
- 调试指南了解连接级调试模式和格式化打印的 SQL。
- 连接管理器指南了解通过
db.manager检查、打开、关闭和修补连接。