简介

模型

本指南是 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 类携带列定义;模型类携带行为。

app/models/user.ts
import { UsersSchema } from '#database/schema'
export default class User extends UsersSchema {}

对应的生成 schema 类如下所示:

database/schema.ts(摘录)
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 遵循一组命名约定,因此大多数模型无需配置即可工作。每个约定都可以通过下一节记录的静态属性覆盖。

概念默认值示例
表名蛇形命名、复数化的模型名UserusersBlogPostblog_posts
主键列idid
列属性蛇形命名列变为驼峰命名属性created_atcreatedAt
关联上的外键蛇形命名单数模型名 + _idUser 通过 user_id 关联
关联上的本地键模型的 primaryKeyid
创建/更新时间戳标记了 autoCreate / autoUpdatecreatedAtupdatedAt使用 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
}

selfAssignPrimaryKeytrue 时,Lucid 在 INSERT 语句中传递主键并将其用于返回的行,而非从数据库读回自动生成的值。

创建、查找和更新实例

最常见模型操作的快速概览。每个操作的深度参考分别在 CRUD 操作查询构建器指南中。

创建行。 Model.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) {
return User.create({
email: request.input('email'),
password: request.input('password'),
})
}
}

查找行。 Model.find 返回具有给定主键的行,或 nullModel.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)

有关完整的查找器集合(findByfindManyfirstOrCreateupdateOrCreatecreateMany*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

一旦模型已保存到数据库(无论是通过 savecreate 还是从查询加载)即为 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

已通过 preloadload 加载的关联关系对象,以关联关系名为键。

$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 不够灵活。

app/services/tenant_service.ts
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 和模型的其余状态,执行验证,并根据需要调用 savedelete

app/models/subscription.ts
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 不会访问数据库,也不需要列来支持它们。

app/models/user.ts
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 操作涵盖 createcreateManyfindOrFailfirstOrCreateupdateOrCreate*Quietly 变体,以及完整的 save 和 delete API。
  • 模型查询构建器涵盖带类型的模型查询、预加载、聚合、has 和 whereHas 过滤、sideload,以及用于纯对象读取的 pojo
  • 查询作用域涵盖附加到模型类的可重用查询助手。
  • 钩子涵盖生命周期钩子(beforeSaveafterCreatebeforeDelete 等),用于强制执行不变量和触发副作用。
  • 序列化模型涵盖模型如何通过 AdonisJS transformers 变为 API 响应的 JSON,包括列表、关联关系和分页数据的实际模式。

有关关联关系建模(belongsTohasManymanyToManyhasManyThroughhasOne),请参见关联关系类别。