Lucid - SQL ORM
本指南介绍 Lucid ORM,即 AdonisJS 官方的数据库 ORM。你将学习如何:
- 配置数据库连接
- 使用查询构建器和模型
- 创建迁移并定义关联关系
- 使用事务和钩子
- 序列化模型并生成测试数据
概述
Lucid ORM 是一个构建在 Knex 之上的 Active Record 风格 ORM,深度集成于 AdonisJS 生态系统中。与需要大量配置的独立 ORM 不同,Lucid 可以与 AdonisJS 的验证器、认证层、缓存、限流和队列等特性无缝协作,而无需任何额外设置。
Lucid 通过用特定的语言和类来封装常见操作,简化了数据库交互。它构建于 Knex 之上,这意味着你在需要时可以借助 JavaScript API 表达复杂的 SQL 查询。Lucid 支持多种数据库,包括 MySQL、PostgreSQL、Turso、SQLite 和 MSSQL。基于类的模型系统让你的代码直观且类型安全,而内建的关联关系支持让你可以建模复杂的数据结构。迁移系统为你的数据库模式提供了版本控制,而 seeders 和 factories 帮助你用测试数据填充数据库。
本指南对 Lucid 的特性进行高层概览,帮助你理解有哪些可用功能以及各部分如何组合。有关详细的 API 参考、高级模式以及特定功能的完整文档,请参阅 Lucid 官方文档 。
配置
Lucid 的配置位于 AdonisJS 项目根目录的 config/database.ts 文件中。该文件定义了你的数据库连接、迁移路径以及其他 ORM 设置。
import env from '#start/env'
import { defineConfig } from '@adonisjs/lucid'
const dbConfig = defineConfig({
connection: env.get('DB_CONNECTION'),
connections: {
postgres: {
client: 'pg',
connection: {
host: env.get('DB_HOST'),
port: env.get('DB_PORT'),
user: env.get('DB_USER'),
password: env.get('DB_PASSWORD'),
database: env.get('DB_DATABASE'),
},
migrations: {
naturalSort: true,
paths: ['database/migrations'],
},
},
},
})
export default dbConfig
配置指定了默认使用哪个数据库连接(通常通过环境变量设置),并定义了每个数据库的连接细节。每个连接都包含客户端库(如 PostgreSQL 的 pg 或 MySQL 的 mysql2)、连接凭据以及迁移文件路径。
你可以在 Lucid 配置文档 中探索所有可用的配置选项、连接池设置以及读写副本等高级特性。
直接使用查询构建器
在深入了解模型之前,你可以直接使用 Lucid 的查询构建器来进行数据库操作。查询构建器提供了一个流畅的 JavaScript API 来构造 SQL 查询,这对于复杂查询或不需要完整 Active Record 模式时特别有用。
查询构建器通过 db 服务可用,并且由于 Lucid 构建于 Knex 之上,它的工作方式与 Knex 完全相同。
import db from '@adonisjs/lucid/services/db'
import type { HttpContext } from '@adonisjs/core/http'
export default class PostsController {
async index({ response }: HttpContext) {
/**
* 选择所有已发布的文章并按创建日期排序。
* 这会返回一个普通对象数组。
*/
const posts = await db
.from('posts')
.select('*')
.where('status', 'published')
.orderBy('created_at', 'desc')
return response.json(posts)
}
async store({ request, response }: HttpContext) {
const { title, content } = request.only(['title', 'content'])
/**
* 插入一篇新文章并返回生成的 ID。
* 插入查询返回的是一个 ID 数组。
*/
const [id] = await db
.insertQuery()
.table('posts')
.insert({
title,
content,
status: 'draft',
created_at: new Date(),
updated_at: new Date(),
})
return response.created({ id })
}
}
查询构建器会自动处理参数化查询,防止 SQL 注入。你可以将其用于查询、插入、更新、删除、连接(join)、聚合以及任何其他 SQL 操作。当你需要原生 SQL 来处理复杂操作时,可以使用 db.rawQuery()。
有关完整的查询构建器 API,包括连接、子查询、聚合和高级 where 子句,请参阅 Lucid 查询构建器文档 。
使用模型
模型提供了一种面向对象的方式来与数据库表交互。每个模型类代表一个表,每个模型实例代表一行。Lucid 采用以迁移为先的方法(migrations-first),你在迁移中定义模式,Lucid 会自动生成你的模型所继承的 TypeScript schema 类。
创建你的第一个迁移
迁移是 Lucid 模式管理的基础。它们为你的数据库模式提供了版本控制,使你可以随着时间推移逐步演进模式。每个迁移都是一个带有 up 和 down 方法的 TypeScript 文件,分别定义如何将模式向前推进以及如何回滚。
为 posts 表创建一个迁移:
node ace make:migration posts
这会在 database/migrations/ 中生成一个带时间戳的迁移文件。时间戳确保迁移以正确的顺序运行。
import { BaseSchema } from '@adonisjs/lucid/schema'
export default class extends BaseSchema {
protected tableName = 'posts'
async up() {
/**
* up 方法创建表结构。
* 使用 schema 构建器来定义列和约束。
*/
this.schema.createTable(this.tableName, (table) => {
table.increments('id')
table.string('title').notNullable()
table.text('content').notNullable()
table.string('status').defaultTo('draft')
table.timestamp('created_at')
table.timestamp('updated_at')
})
}
async down() {
/**
* down 方法撤销 up 方法的更改。
* 这使得在需要时可以回滚迁移。
*/
this.schema.dropTable(this.tableName)
}
}
运行迁移来创建表:
node ace migration:run
Lucid 执行迁移,在你的数据库中创建 posts 表,并自动在 database/schema.ts 生成一个包含所有类型安全列定义的 schema 类。
默认情况下,迁移在事务内部运行。如果迁移失败,所有更改都会自动回滚,使你的数据库保持一致状态。
有关更多迁移操作,如修改表、添加索引和使用外键,请参阅 Lucid 迁移文档 。
自动生成的 schema 类
运行迁移后,Lucid 会扫描你的数据库并生成 TypeScript schema 类,其中包含带有适当类型的所有列定义。这种以迁移为先的方法使你的模型保持简洁,并确保你的 TypeScript 类型始终与实际数据库模式匹配。
生成的 schema 类位于 database/schema.ts。
import { DateTime } from 'luxon'
import { BaseModel, column } from '@adonisjs/lucid/orm'
export class PostsSchema extends BaseModel {
static table = 'posts'
@column({ isPrimary: true })
declare id: number
@column()
declare title: string
@column()
declare content: string
@column()
declare status: string
@column.dateTime({ autoCreate: true })
declare createdAt: DateTime | null
@column.dateTime({ autoCreate: true, autoUpdate: true })
declare updatedAt: DateTime | null
}
Lucid 会自动将数据库类型转换为适当的 TypeScript 类型,将 snake_case 列名转换为 camelCase 属性,并将时间戳列转换为 Luxon DateTime 对象。autoCreate 选项意味着 Lucid 在创建记录时设置时间戳,autoUpdate 意味着它会在每次保存时更新时间戳。
Schema 生成规则
可以通过配置 schema 生成规则来控制默认的 schema 生成。请确保你的数据库配置启用了生成,并指向 database/schema_rules.ts 文件:
import { type SchemaRules } from '@adonisjs/lucid/types/schema_generator';
export default {
types: {
/**
* 全局将所有 JSON 列自定义为使用类型安全的 JSON 包装器,
* 而不是默认的 'any' 类型。
*/
jsonb: {
decorator: '@column()',
tsType: 'JSON<any>',
imports: [{ source: '#types/db', typeImports: ['JSON'] }],
},
},
tables: {
/**
* 自定义 users 表,使 user_role 列
* 成为一个严格的联合类型,而不是通用的 string。
*/
users: {
columns: {
user_role: {
decorators: [{ name: '@column' }],
tsType: `'admin' | 'editor'`,
},
},
},
},
} satisfies SchemaRules;
创建模型
现在创建一个继承生成的 schema 类的模型:
node ace make:model Post
import { PostsSchema } from '#database/schema'
export default class Post extends PostsSchema {
// 你的模型已准备就绪,可使用从 schema 继承的所有列
}
模型从 schema 类继承所有列定义。你会在这里添加关联关系、钩子和自定义方法,而 schema 类负责处理列定义。
切勿直接修改 database/schema.ts 中生成的 schema 类。这些文件在每次迁移后都会重新生成。相反,应该在继承 schema 类并保留你所做更改的模型类中覆盖列或添加自定义逻辑。
如果你需要更改列类型或添加列,请创建一个新的迁移。运行迁移后,schema 类会自动更新。
要了解有关自定义类型映射和 schema 生成规则的更多信息,请参阅 Lucid schema 类文档 。
基础 CRUD 操作
模型准备就绪后,你可以使用直观的 API 执行创建、读取、更新和删除操作。
创建记录:
import Post from '#models/post'
import type { HttpContext } from '@adonisjs/core/http'
export default class PostsController {
async store({ request, response }: HttpContext) {
/**
* 使用 create 方法创建一篇新文章。
* Lucid 会自动设置 created_at 和 updated_at。
*/
const post = await Post.create({
title: request.input('title'),
content: request.input('content'),
status: 'draft',
})
return response.created(post)
}
}
读取记录:
export default class PostsController {
async index({ response }: HttpContext) {
/**
* 获取所有文章并按创建日期排序。
* 返回 Post 模型实例数组。
*/
const posts = await Post.query().orderBy('created_at', 'desc')
return response.json(posts)
}
async show({ params, response }: HttpContext) {
/**
* 根据 ID 查找特定文章。
* 如果未找到则抛出 404 异常。
*/
const post = await Post.findOrFail(params.id)
return response.json(post)
}
}
更新记录:
export default class PostsController {
async update({ params, request, response }: HttpContext) {
const post = await Post.findOrFail(params.id)
/**
* 合并更新并保存到数据库。
* updated_at 时间戳会自动更新。
*/
await post.merge({
title: request.input('title'),
content: request.input('content'),
}).save()
return response.json(post)
}
}
删除记录:
export default class PostsController {
async destroy({ params, response }: HttpContext) {
const post = await Post.findOrFail(params.id)
/**
* 从数据库中删除该文章。
* 这会触发模型上定义的任何删除钩子。
*/
await post.delete()
return response.noContent()
}
}
有关 firstOrCreate、updateOrCreate 等幂等操作,以及 createMany 等批量操作,请参阅
Lucid CRUD 操作文档
。
从模型访问查询构建器
虽然基础 CRUD 方法能处理常见场景,但你经常需要更复杂的查询。每个模型都通过 query() 方法提供对查询构建器的访问,在仍与模型实例协作的同时,为你提供完全的 SQL 灵活性。
import Post from '#models/post'
import type { HttpContext } from '@adonisjs/core/http'
export default class PostsController {
async published({ response }: HttpContext) {
/**
* 使用查询构建器构建复杂查询。
* 结果仍然是 Post 模型实例。
*/
const posts = await Post.query()
.where('status', 'published')
.whereNotNull('published_at')
.where('created_at', '>', new Date('2024-01-01'))
.orderBy('published_at', 'desc')
.limit(10)
return response.json(posts)
}
async search({ request, response }: HttpContext) {
const searchTerm = request.input('q')
/**
* 将带有操作符和多个条件的 where 子句结合使用。
* orWhere 方法向查询添加 OR 条件。
*/
const posts = await Post.query()
.where('title', 'ilike', `%${searchTerm}%`)
.orWhere('content', 'ilike', `%${searchTerm}%`)
.where('status', 'published')
return response.json(posts)
}
}
查询构建器方法返回的是模型实例而非普通对象,这意味着你获得所有模型功能,如关联关系、序列化和钩子。你可以链式调用任何 Knex 查询构建器方法,包括连接、子查询、分组和聚合。
有关完整的查询构建器 API,请参阅 Lucid 查询构建器文档 。
在开发期间美化打印查询
了解你的应用生成了哪些 SQL 查询有助于调试和优化。Lucid 提供了内建的查询调试功能,可在开发期间将 SQL 语句美化打印到你的控制台。
美化打印在开发模式下默认启用。你会在终端中看到格式化的 SQL 查询:
select * from "posts" where "status" = 'published' order by "created_at" desc
该功能在数据库配置中配置:
const dbConfig = defineConfig({
prettyPrintDebugQueries: true,
connections: {
postgres: {
client: 'pg',
connection: {},
/**
* 启用调试模式以记录此连接的所有查询。
* 在生产环境中设置为 false 以避免性能开销。
*/
debug: true,
},
},
})
你也可以在每个查询的基础上启用调试:
const posts = await Post
.query()
.debug(true)
.where('status', 'published')
有关对查询调试和日志记录(包括监听查询事件)的更多控制,请参阅 Lucid 调试文档 。
分页
分页通过以可管理的块加载记录,在处理大型数据集时防止性能问题。Lucid 提供了基于偏移量(offset-based)的分页,可与查询构建器和模型无缝集成。
import Post from '#models/post'
import type { HttpContext } from '@adonisjs/core/http'
export default class PostsController {
async index({ request, response }: HttpContext) {
const page = request.input('page', 1)
const limit = 20
/**
* 每页分页加载 20 条文章。
* 始终使用 orderBy 以确保分页的一致性。
*/
const posts = await Post
.query()
.where('status', 'published')
.orderBy('created_at', 'desc')
.paginate(page, limit)
/**
* 设置分页链接的基础 URL。
* 这样可以生成正确的页面 URL。
*/
posts.baseUrl('/posts')
return response.json(posts)
}
}
分页器提供关于当前页面的元数据:
{
"meta": {
"total": 245,
"perPage": 20,
"currentPage": 1,
"lastPage": 13,
"firstPage": 1,
"firstPageUrl": "/posts?page=1",
"lastPageUrl": "/posts?page=13",
"nextPageUrl": "/posts?page=2",
"previousPageUrl": null
},
"data": [
// Post 对象
]
}
你可以在模板中使用此元数据构建分页 UI:
@each(anchor in posts.getUrlsForRange(1, posts.lastPage))
<a href="{{ anchor.url }}" class="{{ anchor.isActive ? 'active' : '' }}">
{{ anchor.page }}
</a>
@endeach
分页时始终包含 orderBy 子句。如果没有显式排序,数据库引擎可能会以随机顺序返回记录,导致用户在翻页时某些条目出现在多个页面上,或被完全跳过。
有关基于游标(cursor-based)的分页以及自定义 JSON 响应格式,请参阅 Lucid 分页文档 。
事务
数据库事务通过将多个操作分组为一个原子单元来确保数据完整性。如果任何操作失败,整个事务都会回滚,防止可能导致数据库处于不一致状态的局部更新。
Lucid 提供了托管事务(managed transactions),在成功时自动提交,在抛出异常时自动回滚:
import db from '@adonisjs/lucid/services/db'
import Post from '#models/post'
import User from '#models/user'
import type { HttpContext } from '@adonisjs/core/http'
export default class PostsController {
async store({ request, auth, response }: HttpContext) {
/**
* 将操作包裹在事务中。
* 如果任何操作抛出异常,所有更改都会自动回滚。
*/
const post = await db.transaction(async (trx) => {
const user = auth.getUserOrFail()
/**
* 使用事务创建文章。
* 此回调内的所有模型操作都使用同一事务。
*/
const post = new Post()
post.title = request.input('title')
post.content = request.input('content')
post.useTransaction(trx)
await post.save()
/**
* 更新用户的文章计数。
* 这共享同一事务,确保两个操作要么一起成功,要么一起失败。
*/
user.useTransaction(trx)
user.postCount = user.postCount + 1
await user.save()
return post
})
return response.created(post)
}
}
有关手动事务控制、隔离级别和保存点(savepoints),请参阅 Lucid 事务文档 。
模型钩子
钩子允许你在模型生命周期的特定时刻执行代码。你可以使用钩子来在保存前对密码进行哈希处理、验证数据、更新关联记录,或执行任何应该在模型操作期间自动进行的逻辑。
import { UsersSchema } from '#database/schema'
import { beforeSave } from '@adonisjs/lucid/orm'
import hash from '@adonisjs/core/services/hash'
export default class User extends UsersSchema {
/**
* 在保存到数据库之前对密码进行哈希处理。
* 该钩子在插入和更新之前都会运行。
*/
@beforeSave()
static async hashPassword(user: User) {
/**
* 仅在密码被修改时对其进行哈希处理。
* $dirty 对象跟踪哪些属性发生了更改。
*/
if (user.$dirty.password) {
user.password = await hash.make(user.password)
}
}
}
Lucid 为不同的生命周期事件提供钩子。前置钩子(@beforeSave、@beforeCreate、@beforeUpdate、@beforeDelete)在数据库操作之前运行,可以修改数据或取消操作。后置钩子(@afterSave、@afterCreate、@afterUpdate、@afterDelete)在数据库操作之后运行,对于发送通知等副作用很有用。查询钩子(@beforeFind、@afterFind、@beforeFetch、@afterFetch)在获取操作期间运行,可以自动过滤结果。
import { PostsSchema } from '#database/schema'
import { beforeFind, afterCreate } from '@adonisjs/lucid/orm'
export default class Post extends PostsSchema {
/**
* 自动从查询中排除软删除的文章。
* 该钩子修改每个 find 查询以过滤已删除的记录。
*/
@beforeFind()
static ignoreDeleted(query) {
query.where('isDeleted', false)
}
/**
* 在创建文章后发送通知。
* 后置钩子接收已保存的模型实例。
*/
@afterCreate()
static async notifyFollowers(post: Post) {
// 向关注者发送通知
}
}
直接的查询构建器更新会完全绕过钩子。当你使用 await Post.query().where('id', 1).update({ title: 'New' }) 时,不会执行任何钩子,时间戳也不会自动更新。
这种行为的存在是出于更新大量记录时的性能考虑。如果你需要运行钩子,请先获取模型实例,修改它,然后调用 save()。
有关完整的钩子生命周期信息和高级模式,请参阅 Lucid 钩子文档 。
模型关联关系
关联关系定义了你的模型如何相互连接,使处理关联数据变得容易。Lucid 支持一对一、一对多和多对多关联,同时提供懒加载(lazy loading)和预加载(eager loading)。
定义关联关系
一个用户拥有多篇文章:
import { UsersSchema } from '#database/schema'
import { hasMany } from '@adonisjs/lucid/orm'
import Post from '#models/post'
import type { HasMany } from '@adonisjs/lucid/types/relations'
export default class User extends UsersSchema {
/**
* 定义一对多关联关系。
* 一个用户可以拥有多篇文章。
*/
@hasMany(() => Post)
declare posts: HasMany<typeof Post>
}
一篇文章属于一个用户:
import { PostsSchema } from '#database/schema'
import { belongsTo } from '@adonisjs/lucid/orm'
import User from '#models/user'
import type { BelongsTo } from '@adonisjs/lucid/types/relations'
export default class Post extends PostsSchema {
/**
* 定义反向关联关系。
* 每篇文章属于一个用户。
*/
@belongsTo(() => User)
declare user: BelongsTo<typeof User>
}
预加载关联关系
预加载会预先获取关联关系,避免了对每个关联记录执行一个查询的 N+1 查询问题:
import Post from '#models/post'
import type { HttpContext } from '@adonisjs/core/http'
export default class PostsController {
async index({ response }: HttpContext) {
/**
* 为所有文章预加载 user 关联关系。
* 这总共执行两个查询:一个用于文章,一个用于所有用户。
*/
const posts = await Post.query().preload('user')
/**
* 无需额外查询即可访问关联的用户。
* 现在每篇文章都有一个带有已加载数据的 user 属性。
*/
posts.forEach(post => {
console.log(post.user.email)
})
return response.json(posts)
}
}
你可以预加载多个关联关系并嵌套它们:
/**
* 加载 user 及其 profile,以及所有带作者的 comments。
* 嵌套预加载适用于任何深度的关联关系。
*/
const posts = await Post.query()
.preload('user', (query) => {
query.preload('profile')
})
.preload('comments', (query) => {
query.preload('author')
})
多对多关联关系
对于多对多关联,例如用户属于多个团队:
import { UsersSchema } from '#database/schema'
import { manyToMany } from '@adonisjs/lucid/orm'
import Team from '#models/team'
import type { ManyToMany } from '@adonisjs/lucid/types/relations'
export default class User extends UsersSchema {
/**
* 通过数据透视表(pivot table)定义多对多关联关系。
* Lucid 期望一个名为 team_user 的表,包含 user_id 和 team_id 列。
*/
@manyToMany(() => Team, {
pivotColumns: ['role', 'joined_at'],
})
declare teams: ManyToMany<typeof Team>
}
附加和分离关联记录:
const user = await User.findOrFail(1)
/**
* 附加带有额外数据透视数据的团队。
* 数据透视表存储关联关系以及额外的列。
*/
await user.related('teams').attach({
1: { role: 'admin' },
2: { role: 'member' },
})
/**
* sync 只保留指定的团队,并分离其他的。
* 对于"全部保存"风格的操作很有用。
*/
await user.related('teams').sync([1, 2, 3])
有关一对一关联、关联查询以及多态关联等高级模式,请参阅 Lucid 关联关系文档 。
序列化模型
当从 HTTP 端点返回模型时,你需要将它们转换为普通的 JavaScript 对象。Lucid 提供了强大的序列化功能,可以控制输出中出现哪些字段、转换数据以及处理关联关系。
import User from '#models/user'
import type { HttpContext } from '@adonisjs/core/http'
export default class UsersController {
async show({ params, response }: HttpContext) {
const user = await User.query()
.where('id', params.id)
.preload('posts')
.firstOrFail()
/**
* 将模型序列化为普通对象。
* 这会自动排除敏感字段并格式化日期。
*/
return response.json(user.serialize({
fields: {
omit: ['password', 'rememberMeToken'],
},
relations: {
posts: {
fields: {
pick: ['id', 'title', 'createdAt'],
},
},
},
}))
}
}
在模型级别使用列选项控制序列化:
import { UsersSchema } from '#database/schema'
import { column } from '@adonisjs/lucid/orm'
export default class User extends UsersSchema {
/**
* 覆盖 password 列以从所有序列化中排除。
* 将 serializeAs 设置为 null 可防止此字段出现在 JSON 中。
*/
@column({ serializeAs: null })
declare password: string
/**
* 覆盖 firstName 列以在 JSON 输出中重命名。
* 数据库列是 snake_case,但 JSON 使用 camelCase。
*/
@column({ serializeAs: 'firstName' })
declare firstName: string
}
有关自定义序列化逻辑、计算属性以及与 transformers 协作,请参阅 Lucid 序列化文档 和 AdonisJS transformers 文档 。
模型工厂
工厂为测试和数据库填充生成假数据。你无需手动创建测试记录,只需定义一次工厂,即可按需生成逼真的数据。
为你的模型创建工厂:
node ace make:factory Post
import Post from '#models/post'
import Factory from '@adonisjs/lucid/factories'
export const PostFactory = Factory.define(Post, ({ faker }) => {
/**
* 定义如何为每列生成假数据。
* Faker 提供生成逼真假数据的方法。
*/
return {
title: faker.lorem.sentence(),
content: faker.lorem.paragraphs(3),
status: 'draft',
}
}).build()
在测试或 seeders 中使用工厂:
import { PostFactory } from '#database/factories/post_factory'
/**
* 创建一条带有假数据的文章。
* 工厂为每个字段生成随机值。
*/
const post = await PostFactory.create()
/**
* 一次性创建多篇文章。
* 这会生成 10 篇带有唯一假数据的文章。
*/
const posts = await PostFactory.createMany(10)
/**
* 在使用其他字段的假数据的同时,覆盖特定属性。
* 当你需要特定值进行测试时很有用。
*/
const publishedPost = await PostFactory
.merge({ status: 'published' })
.create()
工厂支持用于常见变体的状态(states):
export const PostFactory = Factory.define(Post, ({ faker }) => {
return {
title: faker.lorem.sentence(),
content: faker.lorem.paragraphs(3),
status: 'draft',
}
})
.state('published', (post) => {
post.status = 'published'
post.publishedAt = new Date()
})
.build()
/**
* 使用状态创建已发布的文章。
* 状态提供了工厂可复用的变体。
*/
const publishedPosts = await PostFactory
.apply('published')
.createMany(5)
有关关联工厂、在测试中桩化(stubbing)数据库调用以及高级工厂模式,请参阅 Lucid 工厂文档 。
下一步
本指南涵盖了 Lucid ORM 的基本特性,帮助你理解各部分如何组合。你已经学习了如何配置数据库连接、创建迁移、用自动生成的 schema 定义模型、执行 CRUD 操作、处理关联关系,以及使用工厂生成测试数据。
如需深入了解任何主题,请参阅全面的 Lucid 文档 ,其中涵盖了高级查询构建器方法、关联自定义、查询作用域(query scopes)、软删除、自定义命名策略等等。
你可能还想探索: