事务
本指南涵盖 Lucid 中的 SQL 事务。你将学会如何:
- 使用托管或手动事务将多个查询分组为原子单元
- 设置事务的隔离级别
- 使用嵌套事务创建保存点
- 将事务附加到查询构建器或模型
- 使用事务事件运行提交后钩子
概述
事务将多个数据库操作分组为单个原子单元。事务中的每个操作要么一起提交,要么一起回滚,这在两个或多个写入必须作为一个步骤成功或失败时至关重要。
Lucid 提供两种形式的事务。托管事务接受一个回调,在回调返回时提交,在回调抛出时回滚。手动事务返回一个事务客户端,由你自己提交或回滚。在大多数用例中,优先使用托管事务,因为它们消除了如果代码忘记调用 commit 或 rollback 而导致事务泄漏的可能性。
托管事务
使用异步回调调用 db.transaction。回调接收一个事务客户端(trx),你使用它来发出每个应参与事务的查询。
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.transaction 的解析值。当回调抛出时,Lucid 回滚事务并重新抛出原始错误,以便调用者处理它。
传递第二个参数来设置事务的隔离级别。
await db.transaction(
async (trx) => {
// 使用 trx 的查询
},
{ isolationLevel: 'serializable' }
)
手动事务
当事务的生命周期跨越多个函数边界,或者你需要围绕提交和回滚自定义控制流时,使用手动形式。调用不带参数的 db.transaction() 来获取事务客户端,然后显式提交或回滚。
import db from '@adonisjs/lucid/services/db'
export default class UsersService {
async createWithProfile(email: string, fullName: string) {
const trx = await db.transaction()
try {
const [user] = await trx
.table('users')
.insert({ email })
.returning(['id', 'email'])
await trx.table('profiles').insert({ user_id: user.id, full_name: fullName })
await trx.commit()
return user
} catch (error) {
await trx.rollback()
throw error
}
}
}
你必须始终调用 commit 或 rollback。被遗弃的事务会保持其数据库连接打开,直到连接池强制回收它,这可能会使繁忙服务器上的其他查询资源耗尽。在手动创建事务时,相同的隔离选项也可用。
const trx = await db.transaction({ isolationLevel: 'serializable' })
隔离级别
隔离级别控制事务中所做的更改如何对其他并发事务可见。Lucid 接受以下值,它们映射到相应的 SQL 标准级别。
-
read uncommitted
-
允许读取尚未提交的其他事务中的数据("脏读")。在应用代码中很少有用。PostgreSQL 会自动将此级别提升为
read committed。 -
read committed
-
只有已提交的数据可见,但同一查询在事务内的不同时间点可能返回不同结果,因为其他事务提交了更改。这是 PostgreSQL 的默认隔离级别。
-
snapshot
-
事务读取在开始时获取的数据库一致快照。仅在 MSSQL 上受支持,并且需要在数据库级别设置
ALLOW_SNAPSHOT_ISOLATION ON。在 PostgreSQL 和 MySQL 上使用repeatable read获得类似语义。 -
repeatable read
-
事务在整个过程中读取相同的已提交数据。读取两次的行返回相同的值。这是 MySQL 的默认级别。在 PostgreSQL 上,此级别还会检测某些写入异常并引发序列化失败错误,应用必须通过重试来处理。
-
serializable
-
最严格的级别。事务的行为就像它是唯一一个针对数据库运行的。用于必须看到一致视图并防止并发写入交错的操作。在 PostgreSQL 上,这使用可序列化快照隔离,可能会引发序列化失败错误,你必须重试。
SQLite 使用基于文件级锁定的不同并发模型,而不是隔离级别。在 SQLite 连接上设置 isolationLevel 没有效果。
保存点
在现有事务客户端上调用 transaction() 会创建一个保存点,即外部事务中的一个检查点,可以回滚而不会中止整个事务。Lucid 对两种形式使用相同的 API。
const trx = await db.transaction()
const savepoint = await trx.transaction()
try {
await savepoint.table('audit_logs').insert({ action: 'login' })
await savepoint.commit()
} catch (error) {
await savepoint.rollback()
}
await trx.commit()
回滚保存点使外部事务保持不变,因此在保存点之前通过 trx 发出的任何查询在你调用 trx.commit() 时仍然处于待处理状态。保存点共享外部事务的数据库连接,因此它们不会消耗连接池中的额外连接。
在命名连接上的事务
默认情况下,db.transaction 使用 config/database.ts 中的默认连接。当你的应用使用多个连接时,从 db.connection(name) 启动事务以指定目标。
import db from '@adonisjs/lucid/services/db'
export default class SnapshotService {
async record(total: number) {
await db.connection('analytics').transaction(async (trx) => {
await trx.table('signup_snapshots').insert({ total, recorded_at: new Date() })
})
}
}
返回的事务客户端限定于该连接,并且通过它发出的每个查询都在同一连接上运行。
在查询构建器中使用事务
db 服务上的查询构建器入口点接受一个带有 client 属性的 options 对象。传递事务客户端以将查询路由通过事务。
const trx = await db.transaction()
await db
.query({ client: trx })
.from('users')
.where('id', 1)
.update({ is_active: true })
await db
.insertQuery({ client: trx })
.table('audit_logs')
.insert({ user_id: 1, action: 'activated' })
await trx.commit()
当你已经有一个查询构建器设置好时(例如,在接收到可选 trx 参数的服务方法内部),并希望在调用者启动的事务内运行查询时,使用此模式。
在模型中使用事务
Lucid 模型通过实例上的 useTransaction 方法、查询上的 { client: trx } 选项以及关联关系上的自动继承与事务集成。
将事务附加到模型实例
在保存、删除或执行任何其他写入操作之前,在模型实例上调用 useTransaction。模型将事务存储在 $trx 上,并通过它路由写入操作。
import User from '#models/user'
import db from '@adonisjs/lucid/services/db'
await db.transaction(async (trx) => {
const user = new User()
user.email = 'virk@adonisjs.com'
user.useTransaction(trx)
await user.save()
})
一旦事务提交或回滚,$trx 将重置为 undefined。
将事务附加到模型查询
相同的 { client: trx } 选项适用于模型查询和查找器。
await db.transaction(async (trx) => {
const users = await User.query({ client: trx }).where('is_active', true)
const user = await User.findOrFail(1, { client: trx })
})
当像 findOrFail 这样的查找器返回模型实例时,Lucid 会自动将事务附加到该实例。该实例上的后续 save 或关联关系操作将保持在事务内,无需再次调用 useTransaction。
从模型启动事务
Model.transaction 是一个便捷方法,在模型的连接上启动事务。当整个操作限定于一个模型时,特别是当模型覆盖了 static connection 时,使用它。
import User from '#models/user'
await User.transaction(async (trx) => {
const user = new User()
user.email = 'virk@adonisjs.com'
user.useTransaction(trx)
await user.save()
})
对于默认连接上的模型,这等同于 db.transaction(...)。对于具有自定义 static connection 的模型,Model.transaction 会自动路由到该连接。第二个参数接受与 db.transaction 相同的 isolationLevel 选项。
关联关系继承事务
当你在设置了 $trx 的模型上持久化关联关系时,关联关系查询会隐式继承事务。
await db.transaction(async (trx) => {
const user = new User()
user.email = 'virk@adonisjs.com'
user.useTransaction(trx)
await user.save()
await user.related('profile').create({
fullName: 'Harminder Virk',
avatar: 'some-url.jpg',
})
})
在 profile 查询上不需要额外的 useTransaction 或 { client: trx } 调用。
事务钩子
事务客户端暴露了 after 钩子,在底层提交或回滚完成后运行。使用这些钩子来注册提交后副作用,如缓存失效、任务入队或仅当事务持久化时才应发生的日志记录。
const trx = await db.transaction()
trx.after('commit', async () => {
await cache.invalidate(['users:1'])
})
trx.after('rollback', () => {
logger.warn({ userId: 1 }, 'user update rolled back')
})
after 钩子支持同步和异步处理程序,并按照注册顺序顺序运行。因为钩子在提交已经持久写入后运行,钩子中抛出的任何错误都会被吞掉,以避免在调用者已经看到成功的事务上暴露失败。在钩子内部处理错误,而不是依赖它们传播。
事务客户端也是一个 Node EventEmitter,并在提交/回滚生命周期期间发出 commit 和 rollback 事件。after 钩子涵盖了提交后副作用的用例;仅当你特别需要同步的、钩子前的通知时才使用 trx.on(...)。