模型
本指南是 Lucid 模型的概念入口。你将了解:
- 什么是 Lucid 模型以及何时使用它
- 如何创建模型以及其列定义来自何处
- Lucid 对表、列、主键和时间戳使用的约定
- 覆盖这些约定的静态属性
- 如何创建、查找、更新和删除记录(附深度参考链接)
- 模型如何在生命周期中跟踪状态
- 如何在运行时切换事务和连接
概述
Lucid 模型是一个映射到数据库表的 TypeScript 类。模型的每个实例代表该表中的一行。类暴露查询表的方法,实例暴露保存和删除单行的方法。这种模式称为 Active Record。
import User from '#models/user'
const user = await User.findOrFail(1)
user.email = 'virk@adonisjs.com'
await user.save()
Lucid 模型是查询和持久化数据的默认方式。模型查询构建器暴露与数据库查询构建器相同的查询能力(过滤、连接、聚合、集合操作、CTE),并将结果中的每一行映射回带类型的模型实例。在这些查询之上,模型还为你提供生命周期钩子、关联关系、计算属性和序列化规则,这些与数据一起存在于模型类上。
仅在结果不能是模型实例时才直接使用数据库查询构建器。两种常见情况是不需要模型实例的查询(一次性报表、脚本)以及连接多个表并投影未在任何单个模型上定义的列的查询。
创建模型
模型位于 app/models 中,使用 make:model 生成脚手架。
node ace make:model User
// CREATE: app/models/user.ts
最有用的标志可以在一条命令中生成关联的产物:
--migration生成匹配的迁移--factory生成匹配的工厂--controller生成资源控制器--transformer生成匹配的 transformer
node ace make:model Post --migration --factory --controller
参见命令参考了解完整标志列表。
生成的模型有意为空。所有列定义来自自动生成的 schema 类,而非模型文件。
模型剖析
新的 Lucid 模型继承由 Lucid 从你的实时数据库表生成的 schema 类。Schema 类携带列定义;模型类携带行为。
import { UsersSchema } from '#database/schema'
export default class User extends UsersSchema {}
对应的生成 schema 类如下所示:
import { BaseModel, column } from '@adonisjs/lucid/orm'
import { DateTime } from 'luxon'
export class UsersSchema extends BaseModel {
static table = 'users'
@column({ isPrimary: true })
declare id: number
@column()
declare email: string
@column({ serializeAs: null })
declare password: string
@column.dateTime({ autoCreate: true })
declare createdAt: DateTime
@column.dateTime({ autoCreate: true, autoUpdate: true })
declare updatedAt: DateTime
}
Schema 类在每次迁移运行时都会重新生成,因此你添加到其中的任何代码都会丢失。将自定义代码保留在模型文件中。这包括关联关系、钩子、查询作用域、计算属性、自定义方法和序列化覆盖。Schema 类定义列结构,模型类处理其他所有事务。
有关完整的 schema 生成工作流、类型映射和自定义,请参见 schema 类指南。
约定
Lucid 遵循一组命名约定,因此大多数模型无需配置即可工作。每个约定都可以通过下一节记录的静态属性覆盖。
| 概念 | 默认值 | 示例 |
|---|---|---|
| 表名 | 蛇形命名、复数化的模型名 | User → users,BlogPost → blog_posts |
| 主键列 | id | id |
| 列属性 | 蛇形命名列变为驼峰命名属性 | created_at → createdAt |
| 关联上的外键 | 蛇形命名单数模型名 + _id | User 通过 user_id 关联 |
| 关联上的本地键 | 模型的 primaryKey | id |
| 创建/更新时间戳 | 标记了 autoCreate / autoUpdate 的 createdAt 和 updatedAt 列 | 使用 table.timestamps(true, true) 生成 |
当数据库不匹配这些约定时(现有 schema、第三方工具、多租户列前缀),覆盖不匹配的特定约定。
模型配置
通过在类上设置静态属性来配置模型。下面的每个静态属性都有合理的默认值;你只需声明想要覆盖的那些。
-
table
-
数据库表名。默认为蛇形命名、复数化的模型类名。当你的模型指向非约定表时覆盖。
export default class User extends UsersSchema {static table = 'app_users'} -
primaryKey
-
唯一标识一行的列。默认为
'id'。主键列也用作关联关系的本地键。export default class User extends UsersSchema {static primaryKey = 'user_id'} -
connection
-
模型查询使用的命名数据库连接。默认为应用的主连接。当模型位于非默认连接上时设置此属性(分析数据库、读副本池、租户数据库)。
export default class AnalyticsEvent extends AnalyticsEventsSchema {static connection = 'analytics'}Lucid 将此值用于模型发出的每个查询,包括关联关系和
Model.transaction(...)调用。 -
selfAssignPrimaryKey
-
当你的应用生成主键值而非数据库时设置为
true。默认为false。使用 UUID、ULID 或任何应用生成标识符的模型需要此设置。export default class User extends UsersSchema {static selfAssignPrimaryKey = true}当
selfAssignPrimaryKey为true时,Lucid 在 INSERT 语句中传递主键并将其用于返回的行,而非从数据库读回自动生成的值。
创建、查找和更新实例
最常见模型操作的快速概览。每个操作的深度参考分别在 CRUD 操作和查询构建器指南中。
创建行。 Model.create 构建实例、持久化并返回它。
import type { HttpContext } from '@adonisjs/core/http'
import User from '#models/user'
export default class UsersController {
async store({ request }: HttpContext) {
return User.create({
email: request.input('email'),
password: request.input('password'),
})
}
}
查找行。 Model.find 返回具有给定主键的行,或 null。Model.findOrFail 在没有匹配行时抛出异常,AdonisJS 会自动将其转换为 404 响应。
const user = await User.findOrFail(params.id)
更新行。 修改模型的属性,然后调用 save。Lucid 仅写入实际更改的列。
const user = await User.findOrFail(params.id)
user.email = request.input('email')
await user.save()
删除行。 在已加载的实例上调用 delete。
const user = await User.findOrFail(params.id)
await user.delete()
查询多行。 Model.query() 返回限定在该模型范围内的带类型查询构建器。
const activeUsers = await User
.query()
.where('is_active', true)
.orderBy('createdAt', 'desc')
.limit(10)
有关完整的查找器集合(findBy、findMany、firstOrCreate、updateOrCreate、createMany、*Quietly 变体),请参见 CRUD 操作。有关包括预加载、作用域和聚合的带类型查询构建器,请参见模型查询构建器。
模型状态
Lucid 模型携带的不仅是列值。每个实例还跟踪行是否已持久化、自模型加载以来哪些属性已被修改、哪些关联关系已预加载,以及查询返回的任何额外列。
理解这些状态属性的最简单方式是遍历持久化生命周期。
const user = new User() // 本地的,未持久化,非 dirty
user.email = 'virk@adonisjs.com' // dirty: { email }
user.password = 'secret' // dirty: { email, password }
await user.save() // 执行 INSERT;现在已持久化,dirty 已清除
user.email = 'new@adonisjs.com' // 再次 dirty: { email }
console.log(user.$original.email) // 'virk@adonisjs.com'
console.log(user.$attributes.email) // 'new@adonisjs.com'
await user.save() // 执行 UPDATE;仅发送 email
await user.delete() // 执行 DELETE;设置 isDeleted
两对属性驱动变更跟踪:
$attributes保存模型的当前列值。给属性赋值如user.email = '...'会更新$attributes。$original保存模型加载时的值(或最近一次save后的值)。它是$dirty比较的基线。
其余属性标记模型的生命周期状态:
-
$dirty
-
仅包含自模型加载或上次保存以来已更改的列的对象。没有任何更改时为空。
-
$isDirty
-
当
$dirty有任何键时为true。当你需要布尔检查而无需读取 dirty 对象时很方便。 -
$isPersisted
-
一旦模型已保存到数据库(无论是通过
save、create还是从查询加载)即为true。从查询加载的模型默认是持久化的。 -
$isLocal
-
对于在应用代码中构造的模型(使用
new User()或Model.create)为true,对于从数据库加载的模型为false。对于仅应为新记录运行的beforeSave钩子很有用。 -
$isNew
-
与
$isPersisted相反。当模型从未被保存时为true。 -
$isDeleted
-
一旦
delete()被调用且行被移除即为true。对已删除模型后续调用save会抛出异常。 -
$primaryKeyValue
-
模型主键列的当前值,无论主键是哪个列。等同于
model[Model.primaryKey]。 -
$extras
-
查询返回但未在模型上声明的额外列。连接的列和聚合存储在此处。例如,
withCount('posts')填充model.$extras.posts_count。 -
$preloaded
-
已通过
preload或load加载的关联关系对象,以关联关系名为键。 -
$sideloaded
-
通过
sideload方法在查询中传播的任意键值对对象。用于将请求作用域上下文(当前用户、租户 ID)附加到查询结果中的每个模型。 -
$trx
-
此模型当前绑定的事务客户端,或
undefined。由useTransaction设置或通过从事务中读取模型设置。 -
$options
-
当前模型选项(连接名、profiler 上下文)。主要在钩子和适配器内部有用。
运行时自定义
三个实例方法允许你调整单个模型与数据库的交互方式,而无需修改其静态配置。
refresh
从数据库重新读取模型的行并覆盖本地属性。在触发了数据库端默认值、生成列或应用代码未看到的基于触发器的变更的写入之后很有用。
const post = await Post.create({ title: 'Hello' })
// `slug` 由数据库触发器生成
await post.refresh()
console.log(post.slug)
useTransaction
将模型实例绑定到事务客户端,使其写入参与事务的提交或回滚。参见事务指南了解完整模式。
await db.transaction(async (trx) => {
const user = new User()
user.email = 'virk@adonisjs.com'
user.useTransaction(trx)
await user.save()
})
当你从事务绑定的查询中加载模型时,$trx 会自动设置,你无需在结果上再次调用 useTransaction。
useConnection
在运行时将模型绑定到非默认连接。用于按请求解析租户数据库的多租户应用,其中设置 static connection 不够灵活。
import User from '#models/user'
export default class TenantService {
static forTenant(tenantConnection: string) {
const user = new User()
user.useConnection(tenantConnection)
return user
}
}
该设置仅应用于模型实例;User.query() 的后续查询继续使用默认连接。
添加自定义行为
生成的 schema 类定义你的模型具有的列。模型文件是你添加其他所有内容的地方:领域方法、派生属性和状态依赖逻辑。将自定义代码保留在此处,而非生成的 schema 文件中,这样你的编辑可以在重新生成后保留。
自定义方法
实例方法允许你表达属于模型的操作。它们可以读取 this.$attributes 和模型的其余状态,执行验证,并根据需要调用 save 或 delete。
import { DateTime } from 'luxon'
import { SubscriptionsSchema } from '#database/schema'
export default class Subscription extends SubscriptionsSchema {
isActive(): boolean {
return this.status === 'active' && this.expiresAt > DateTime.now()
}
async cancel(reason: string) {
this.status = 'cancelled'
this.cancellationReason = reason
this.cancelledAt = DateTime.now()
await this.save()
}
}
静态方法遵循相同的模式,存在于类本身上。用于操作模型类型而非特定实例的行为。
import { SubscriptionsSchema } from '#database/schema'
export default class Subscription extends SubscriptionsSchema {
static activeForUser(userId: number) {
return this.query().where('user_id', userId).where('status', 'active')
}
}
使用 getter 的派生属性
TypeScript getter 允许你暴露从现有列计算的派生值。Getter 不会访问数据库,也不需要列来支持它们。
import { UsersSchema } from '#database/schema'
export default class User extends UsersSchema {
get fullName(): string {
return `${this.firstName} ${this.lastName}`.trim()
}
get initials(): string {
return `${this.firstName?.[0] ?? ''}${this.lastName?.[0] ?? ''}`.toUpperCase()
}
}
覆盖实际列的 Getter(例如,为存储的路径返回计算的 URL)是列覆盖模式,属于 schema 类指南而非此处。
后续阅读
每个重点领域都有自己的指南:
- Schema 类涵盖自动生成的
database/schema.ts如何工作、每种数据库列类型的类型映射、类型自定义的 schema 规则以及模型级列覆盖。 - CRUD 操作涵盖
create、createMany、findOrFail、firstOrCreate、updateOrCreate、*Quietly变体,以及完整的 save 和 delete API。 - 模型查询构建器涵盖带类型的模型查询、预加载、聚合、has 和 whereHas 过滤、sideload,以及用于纯对象读取的
pojo。 - 查询作用域涵盖附加到模型类的可重用查询助手。
- 钩子涵盖生命周期钩子(
beforeSave、afterCreate、beforeDelete等),用于强制执行不变量和触发副作用。 - 序列化模型涵盖模型如何通过 AdonisJS transformers 变为 API 响应的 JSON,包括列表、关联关系和分页数据的实际模式。
有关关联关系建模(belongsTo、hasMany、manyToMany、hasManyThrough、hasOne),请参见关联关系类别。