CRUD 操作

CRUD 操作

本指南涵盖 Lucid 模型的 CRUD API。你将学会如何:

  • 单条或批量创建记录
  • 使用 findfindByfirst 及其 orFail 变体读取记录
  • 通过先获取再保存模式或使用查询构建器更新记录
  • 逐条或批量删除记录
  • 使用 find-or-create 和 update-or-create 辅助方法
  • 使用 Quietly 变体跳过生命周期钩子

概述

Lucid 模型将 CRUD 操作作为类上的静态方法和已加载模型的实例方法公开。静态方法处理常见场景(Model.createModel.findOrFailModel.updateOrCreate);实例方法操作已加载的行(user.saveuser.delete)。通过 Model.query() 访问的模型查询构建器处理所有不适合单方法调用的查询。完整参考请参见模型查询构建器

创建记录

create

通过传入列值对象来持久化新记录。该方法插入一行,运行每个已配置的钩子,并返回模型实例。

app/controllers/users_controller.ts
import type { HttpContext } from '@adonisjs/core/http'
import User from '#models/user'
export default class UsersController {
async store({ request }: HttpContext) {
const user = await User.create({
email: request.input('email'),
password: request.input('password'),
})
return user
}
}

先构建实例

创建实例,逐个赋值列,然后通过 save 持久化。第一次 save 调用执行 INSERT;后续 save 调用执行 UPDATE

const user = new User()
user.email = 'virk@adonisjs.com'
user.password = 'secret'
await user.save()

fillmerge 可一次在未保存或已加载的实例上设置多个属性。它们的行为不同:fill 在设置新值之前会完全替换模型的属性,而 merge 仅更新你传入的键,其余保持不变。

const user = new User()
// 保存前设置多个属性的等效方式
await user.fill({ email, password }).save()
await user.merge({ email, password }).save()
// 在已加载的实例上,通常你需要的是 merge
const existing = await User.findOrFail(1)
existing.merge({ email: newEmail }).save()

当你想在赋新值之前完全重置实例时使用 fill。当你想更新列的子集并保持实例其余状态不变时使用 merge

createMany

持久化一组记录。createMany 开启一个托管事务,逐行插入(因此每个模型的钩子和时间戳都会运行),并一起提交整批数据。如果任何插入失败,事务会回滚。

const users = await User.createMany([
{ email: 'virk@adonisjs.com', password: 'secret' },
{ email: 'romain@adonisjs.com', password: 'secret' },
])

由于每行都是单独插入的,总查询数随批量大小增长。对于不需要钩子的大规模导入,改用 insert 查询构建器中的 db.table('users').multiInsert(...),它只生成一条语句。

allowExtraProperties

默认情况下,传入模型未声明的键会抛出错误。设置 allowExtraProperties: true 以静默丢弃未知键。当转发可能包含客户端字段的 HTTP 请求载荷时很有用,这些字段不会映射到数据库。

await User.create(request.all(), { allowExtraProperties: true })
// 或在实例上
user.fill(request.all(), true).save()

createQuietly 和 createManyQuietly

Quietly 变体在不运行钩子的情况下插入记录。当必须跳过钩子时使用,例如在预计算钩子本应生成的值的种子器中,或在测试 fixture 设置期间。

await User.createQuietly({ email, password })
await User.createManyQuietly([
{ email: 'a@example.com', password: 'secret' },
{ email: 'b@example.com', password: 'secret' },
])

跳过钩子仅适用于插入本身。你在回调中触发的关联模型操作仍然会运行自己的钩子。

读取记录

all

从表中获取每一行。

const users = await User.all()
// SQL: SELECT * FROM "users" ORDER BY "id" DESC

find 和 findOrFail

按主键查找行。find 在没有匹配行时返回 nullfindOrFail 抛出 E_ROW_NOT_FOUND(AdonisJS 将其转换为 404 响应)。

const user = await User.find(1) // User | null
const user = await User.findOrFail(1) // User,否则 404

findBy 和 findByOrFail

按任意列查找行。支持两种调用形式:列 + 值,或用于复合查找的列值对对象。

const user = await User.findBy('email', 'virk@adonisjs.com')
const user = await User.findBy({
email: 'virk@adonisjs.com',
is_active: true,
})

findByOrFail 有相同的两种签名,在没有匹配行时抛出异常。

const user = await User.findByOrFail('email', 'virk@adonisjs.com')

findMany 和 findManyBy

findMany 按主键加载多行。findManyBy 加载匹配一个或多个列的多行(与 findBy 相同的两种签名)。

const users = await User.findMany([1, 2, 3])
const activeUsers = await User.findManyBy('is_active', true)
const activeAdmins = await User.findManyBy({
is_active: true,
role: 'admin',
})

findMany 不保留输入数组的顺序。返回的行按主键排序(降序)。不要假设 findMany([2, 1]) 会先返回 id = 2 的行。当顺序很重要时,使用 Model.query().whereIn(...).orderBy(...) 构建查询,或在应用代码中对结果排序。

first 和 firstOrFail

从表中获取第一行。没有查询约束时,这在大多数方言上返回主键最小的行。

const user = await User.first() // User | null
const user = await User.firstOrFail() // User,否则抛出异常

使用查询构建器

静态查找器覆盖了常见场景。对于更复杂的需求(排序、连接、作用域、预加载、条件过滤),使用模型查询构建器。

const users = await User
.query()
.where('is_active', true)
.orWhereNotNull('subscription_plan')
.orderBy('created_at', 'desc')
const user = await User
.query()
.where('email', email)
.firstOrFail()

完整 API 请参见模型查询构建器指南,包括预加载、聚合和查询作用域。

更新记录

获取、修改、保存

标准模式是加载行,更改你要更新的属性,然后调用 save。Lucid 通过 $dirty 跟踪哪些列发生了变化,只写入这些列。

const user = await User.findOrFail(params.id)
user.email = request.input('email')
await user.save()

当你想一次应用多个更改时使用 merge + save

await user.merge({
email: request.input('email'),
lastLoginAt: DateTime.now(),
}).save()

saveQuietly

在不运行 beforeSaveafterSavebeforeUpdateafterUpdate 钩子的情况下保存。在填充种子、恢复备份或更新不应触发钩子副作用的列时使用。

user.lastSyncedAt = DateTime.now()
await user.saveQuietly()

使用查询构建器批量更新

当你需要一次修改大量行时(标记过期会话、归档旧行、重置标志),使用模型查询构建器的 update。它发出一条 UPDATE 语句并跳过模型钩子和自动时间戳。

await User.query().where('is_active', false).update({
status: 'archived',
archived_at: DateTime.now().toSQL(),
})

由于此路径绕过模型钩子,你的钩子强制执行的任何不变量(密码哈希、审计日志插入、计算列)都不会运行。当每行都需要完整的模型管道时使用实例模式;当行仅由 SQL 管理时使用查询构建器路径。

删除记录

实例删除

加载行并调用 delete。实例的 $isDeleted 标志变为 true

const user = await User.findOrFail(params.id)
await user.delete()

deleteQuietly

在不运行 beforeDeleteafterDelete 钩子的情况下删除。

await user.deleteQuietly()

使用查询构建器批量删除

对于不应在每行上运行钩子的删除,通过查询构建器发出删除。

await User.query().where('is_verified', false).delete()

与批量更新相同的注意事项适用:钩子和自动时间戳列会被跳过。

Upsert 和 find-or-create 辅助方法

这些静态方法将查找与条件创建或更新结合在一起。它们都接受一个单独的搜索载荷(用于查找现有行的键)和一个可选的保存载荷(创建新行或更新现有行时写入的值)。

firstOrCreate

查找匹配搜索载荷的行。没有匹配行时,使用合并的搜索和保存载荷创建一行。返回现有的或新创建的模型。

const user = await User.firstOrCreate(
{ email: 'virk@adonisjs.com' }, // 搜索
{ password: 'secret' } // 创建时的额外值
)

如果该邮箱的用户已存在,则原样返回,password 被忽略。如果没有匹配,Lucid 插入一行同时包含邮箱和密码。

firstOrNew

firstOrCreate 相同的查找,但当没有匹配行时,Lucid 返回一个新的未保存实例,包含合并的载荷。当你想在持久化之前做更多修改,或决定是否保存时很有用。

const user = await User.firstOrNew(
{ email: 'virk@adonisjs.com' },
{ password: 'secret' }
)
if (user.$isNew) {
user.source = 'invitation'
await user.save()
}

updateOrCreate

查找匹配搜索载荷的行并用保存载荷更新它,或在没有匹配时用合并的载荷插入新行。

await User.updateOrCreate(
{ email: 'virk@adonisjs.com' },
{ lastLoginAt: DateTime.now() }
)

firstOrCreate 不同,updateOrCreate 始终写入数据库,要么是 INSERT 语句,要么是 UPDATE 语句。

fetchOrCreateMany

firstOrCreate 的批量版本。传入标识行的唯一键(或键数组),加上一组对象。对于每个对象,Lucid 按键查找现有行,没有匹配行时插入。现有行原样返回不变。

await User.fetchOrCreateMany('email', [
{ email: 'foo@example.com', name: 'Foo' },
{ email: 'bar@example.com', name: 'Bar' },
])

复合键使用数组:

await Post.fetchOrCreateMany(['tenant_id', 'slug'], [
{ tenant_id: 1, slug: 'hello', body: '...' },
{ tenant_id: 1, slug: 'world', body: '...' },
])

fetchOrNewUpMany

fetchOrCreateMany 相同,但不存在的行作为未保存的实例返回而非插入。对于预览流程或将持久化延迟到显式确认之后很有用。

const users = await User.fetchOrNewUpMany('email', [
{ email: 'foo@example.com', name: 'Foo' },
{ email: 'bar@example.com', name: 'Bar' },
])
// 部分项可能是未保存的实例
users.filter((user) => user.$isNew)

updateOrCreateMany

updateOrCreate 的批量版本。每个对象按给定键匹配。现有行被更新;缺失的行被插入。

await User.updateOrCreateMany('email', [
{ email: 'foo@example.com', name: 'Foo' },
{ email: 'bar@example.com', name: 'Bar' },
])

复合键在这里也适用,通常用于同步父 + 子组合。

await SubscriptionDay.updateOrCreateMany(['subscription_id', 'day'], days)

所有四个批量辅助方法(fetchOrCreateManyfetchOrNewUpManyupdateOrCreateManycreateMany)都将写入操作包装在托管事务中,因此批量操作是全有或全无的。

清空表

Model.truncate 清空底层表。在测试设置和拆卸中使用它来重置到干净状态。在支持的方言上(PostgreSQL、MSSQL)传入 true 以级联清空关联表。

await User.truncate()
await User.truncate(true) // 级联

Truncate 绕过模型钩子,不会触发 beforeDeleteafterDelete。它比 DELETE FROM users 更快,因为大多数数据库会重置自增计数器并跳过逐行日志记录,但两者并非在所有方言中都等价。