模型查询构建器
本指南涵盖模型查询构建器独有的方法。你将学会如何:
- 在一层、两层或多层中预加载关联关系
- 使用
withCount和withAggregate加载关联关系聚合 - 使用
has和whereHas按关联关系的存在性过滤行 - 使用模型作用域组合可重用查询
- 使用
pojo、sideload和rowTransformer塑造返回的行 - 为 API 响应和模板分页模型查询
概述
Model.query() 返回的模型查询构建器扩展了查询构建器部分记录的数据库查询构建器。数据库查询构建器上的每个方法在此都可用,包括 where、whereIn、orderBy、join、聚合、锁、CTE 和条件助手。本指南仅涵盖模型查询构建器独有的方法或因模型感知而行为不同的方法。
两件事使模型查询构建器不同于数据库查询构建器:
- 它返回的每一行都是带类型的模型实例,而非普通对象。
- 它可以在单个链中查询关联关系、聚合它们、按存在性过滤,并将它们水合到父行上。
const posts = await Post
.query()
.where('is_published', true)
.preload('author')
.withCount('comments')
.orderBy('created_at', 'desc')
预加载关联关系
preload
加载关联关系并将其附加到每个返回的模型实例上。预加载对每个关联关系发出一条额外查询,无论返回了多少父记录,这避免了 N+1 问题。
const users = await User.query().preload('posts')
for (const user of users) {
console.log(user.posts.length)
}
传入回调以约束关联关系查询。回调接收关联模型的查询构建器,因此每个过滤和排序方法都可用。
await User.query().preload('posts', (postsQuery) => {
postsQuery.where('status', 'published').orderBy('published_at', 'desc')
})
通过链式调用预加载多个关联关系。
await User.query().preload('posts').preload('profile').preload('roles')
嵌套预加载通过在回调内部再次调用 preload 来工作。每个深度的每个关联关系仍然只发出一条额外查询,而非每个父记录一条。
await User
.query()
.preload('posts', (postsQuery) => {
postsQuery.preload('comments', (commentsQuery) => {
commentsQuery.preload('author')
})
})
preloadOnce
仅当关联关系尚未在此查询上预加载时才预加载它。在共享助手或作用域中很有用,多个调用者可能请求相同的关联关系,但你只想加载它一次。
Post.query().preloadOnce('author')
在同一查询上调用 preload('author') 两次会运行两条查询并覆盖第一个结果。preloadOnce('author') 保证关联关系最多加载一次。
关联关系聚合
withCount
运行一个计算关联关系数量的关联子查询,并将结果附加到每个父记录的 $extras 上。
const users = await User.query().withCount('posts')
users.forEach((user) => {
console.log(user.$extras.posts_count)
})
默认别名是 {relationName}_count。通过回调覆盖它。
const user = await User
.query()
.withCount('posts', (query) => {
query.where('status', 'published').as('publishedPostsCount')
})
.firstOrFail()
console.log(user.$extras.publishedPostsCount)
withCount 和 preload 是独立的。当你同时需要完整列表和计数时,两者都调用。
await User.query().preload('posts').withCount('posts')
withAggregate
使用自定义聚合(sum、min、max、avg)运行关联关系子查询。在回调内部使用 .as(...) 定义别名。
const user = await User
.query()
.withAggregate('accounts', (query) => {
query.sum('balance').as('accountsBalance')
})
.firstOrFail()
console.log(user.$extras.accountsBalance)
回调必须恰好调用一个聚合并设置别名。聚合结果以别名存放在 $extras 上。
按关联关系过滤
has
按关联关系是否返回任何行来过滤父行。"至少有一篇文章的用户"、"属于某个分类的产品"等等。
const authors = await User.query().has('posts')
传入运算符和计数以按关联关系大小过滤。
const prolificAuthors = await User.query().has('posts', '>=', 5)
该系列涵盖组合和取反变体:
| 方法 | 描述 |
|---|---|
has / andHas | WHERE 关联关系有行 |
orHas | OR WHERE 关联关系有行 |
doesntHave / andDoesntHave | WHERE 关联关系没有行 |
orDoesntHave | OR WHERE 关联关系没有行 |
whereHas
按关联关系的存在性过滤,并对关联关系查询附加额外约束。第二个参数是细化关联关系子查询的回调。
const activeCommenters = await User.query().whereHas('comments', (commentsQuery) => {
commentsQuery.where('created_at', '>', lastWeek)
})
与运算符和计数结合使用,要求特定数量的匹配关联行。
const veteranCommenters = await User.query().whereHas(
'comments',
(commentsQuery) => commentsQuery.where('is_approved', true),
'>',
10
)
变体与 has 系列对应:
| 方法 | 描述 |
|---|---|
whereHas / andWhereHas | WHERE 关联关系有匹配行 |
orWhereHas | OR WHERE 关联关系有匹配行 |
whereDoesntHave / andWhereDoesntHave | WHERE 关联关系没有匹配行 |
orWhereDoesntHave | OR WHERE 关联关系没有匹配行 |
查询作用域
查询作用域是定义在模型类上的可重用查询片段。查询作用域指南涵盖如何定义它们;查询构建器通过 withScopes 或其别名 apply 来应用它们。
import { scope } from '@adonisjs/lucid/orm'
import { TeamsSchema } from '#database/schema'
export default class Team extends TeamsSchema {
static forUser = scope((query, user: User) => {
query.whereIn(
'id',
db.from('user_teams').select('team_id').where('user_teams.user_id', user.id),
)
})
}
使用 withScopes 应用作用域。回调接收一个作用域包装器,它将模型上定义的每个作用域作为带类型的方法暴露。
const teams = await Team
.query()
.withScopes((scopes) => scopes.forUser(auth.user))
apply(callback) 是 withScopes(callback) 的别名,在一条链中组合多个作用域时更易读。
const teams = await Team
.query()
.apply((scopes) => {
scopes.forUser(auth.user)
scopes.active()
})
转换结果
pojo
返回普通 JavaScript 对象而非模型实例。查询变为 db.from(...) 读取的带类型等价物,这对于水合模型实例的开销不合理的读取密集型端点很有用。
const posts = await Post.query().pojo()
console.log(posts[0] instanceof Post) // false
console.log(posts[0].title) // 仍然带类型
当查询使用 pojo 时,生命周期钩子(beforeFind、afterFind、beforeFetch、afterFetch)不会运行,因为钩子操作的是模型实例。
sideload
将任意键值对附加到查询返回的每个模型实例上。值存放在每个实例的 $sideloaded 属性上,并传播到预加载的关联关系。
const posts = await Post
.query()
.sideload({ currentUser: auth.user })
.preload('comments')
posts.forEach((post) => {
console.log(post.$sideloaded.currentUser) // auth 用户
post.comments.forEach((comment) => {
console.log(comment.$sideloaded.currentUser) // 也已设置
})
})
默认情况下 sideload 完全替换对象。传入 true 作为第二个参数以合并到现有的 sideloaded 值中。
Post.query().sideload({ a: 1 }).sideload({ b: 2 }, true)
// 结果: $sideloaded = { a: 1, b: 2 }
rowTransformer
注册一个在加载后、查询解析前对每个模型实例运行的回调。用于在不经过钩子或模型访问器的情况下用计算值装饰行。
const users = await User
.query()
.preload('subscription')
.rowTransformer((user) => {
user.$extras.daysRemaining = user.subscription
? user.subscription.expiresAt.diff(DateTime.now(), 'days').days
: 0
})
回调接收模型实例并可以就地修改它。当转换应应用于每次加载时优先使用钩子;当转换特定于单个查询的形状时优先使用 rowTransformer。
分页
paginate(page, perPage) 使用 LIMIT 和 OFFSET 运行查询,执行单独的 COUNT 查询,并返回分页器。返回值是 ModelPaginator:它是模型实例数组,加上与数据库查询构建器的 SimplePaginator 相同的元属性和方法。
import type { HttpContext } from '@adonisjs/core/http'
import Post from '#models/post'
import PostTransformer from '#transformers/post_transformer'
export default class PostsController {
async index({ request, inertia }: HttpContext) {
const page = request.input('page', 1)
const posts = await Post.query()
.orderBy('created_at', 'desc')
.paginate(page, 20)
return inertia.render('posts/index', {
posts: PostTransformer.paginate(posts.all(), posts.getMeta()),
})
}
}
参见分页指南了解完整的分页器参考、URL 自定义、API 和 Inertia 响应的 transformer 集成以及性能考虑。
查询期间触发的钩子
模型查询构建器在模型上调用以下钩子:
| 钩子 | 触发时机 |
|---|---|
beforeFind / afterFind | first()、firstOrFail() 以及使用它们的静态 find/findBy 系列 |
beforeFetch / afterFetch | 当查询通过 await 或 .exec() 解析为多行时 |
beforePaginate / afterPaginate | 在 paginate() 周围,分页结果解析前后 |
当使用 pojo() 时钩子不会触发,因为钩子操作的是模型实例。参见钩子指南了解如何定义和使用它们。
model 属性
每个模型查询构建器在其 model 属性上暴露其来源模型类。在通用地操作任何模型的助手内部很有用。
const query = User.query()
console.log(query.model === User) // true