查询范围

查询作用域

本指南涵盖 Lucid 模型上的查询作用域。你将学会如何:

  • 在模型上将可重用的查询片段定义为作用域
  • 通过 withScopesapply 应用作用域
  • 在调用时向作用域传递参数
  • 在其他作用域内部组合作用域
  • 在预加载回调中重用作用域

概述

查询作用域是附加到模型上的可重用查询逻辑片段。作用域将常用的重复 where 子句和连接从控制器和服务中移出,在模型上统一命名。你可能会定义的一些作用域示例:

  • Post 模型上的 published,添加 where published_at <= now()
  • Project 模型上的 visibleTo(user),按当前用户的团队过滤
  • Subscription 模型上的 active,同时检查状态和过期时间

作用域定义为模型类上的静态属性,使用 scope() 助手包装。查询构建器通过带类型的访问器暴露它们,因此每次调用都保持类型安全。

定义作用域

使用 scope() 助手包装回调并将其赋值给模型上的静态属性。回调接收模型的查询构建器作为第一个参数,可以应用任何查询构建器方法。

app/models/post.ts
import { DateTime } from 'luxon'
import { scope } from '@adonisjs/lucid/orm'
import { PostsSchema } from '#database/schema'
export default class Post extends PostsSchema {
static published = scope((query) => {
query.where('publishedOn', '<=', DateTime.utc().toSQLDate())
})
}

scope() 助手纯粹为 TypeScript 而存在。它在运行时是空操作,并添加一个类型标记,作用域访问器使用该标记进行自动补全和参数检查。没有 scope() 时,任何函数值的静态属性仍然可以通过运行时作用域包装器调用,但 TypeScript 不会在带类型的访问器上列出它或检查其参数类型。

应用作用域

通过 withScopes(callback) 或其别名 apply(callback) 应用作用域。回调接收一个作用域包装器,它将模型上的每个作用域作为带类型的方法暴露。

const posts = await Post.query().withScopes((scopes) => scopes.published())

当你在一条链中组合多个作用域时,apply 更易读。

const posts = await Post
.query()
.apply((scopes) => {
scopes.published()
scopes.popular()
})
.orderBy('published_on', 'desc')

两种方法都返回查询构建器,因此它们可以与每个其他查询方法链式调用。

带参数的作用域

作用域回调在 query 之后接受额外参数。在应用作用域时传递它们。

app/models/project.ts
import User from '#models/user'
import { scope } from '@adonisjs/lucid/orm'
import { ProjectsSchema } from '#database/schema'
export default class Project extends ProjectsSchema {
static visibleTo = scope((query, user: User) => {
if (user.isAdmin) {
return
}
query.where('teamId', user.teamId)
})
}

使用所需参数应用。

const projects = await Project
.query()
.withScopes((scopes) => scopes.visibleTo(auth.user))

作用域包装器根据作用域的签名进行类型化,因此参数类型在调用点强制执行。

组合作用域

一个作用域可以通过同一个查询构建器调用另一个作用域,这使组合保持透明。回调已经接收了模型的查询构建器,因此 query.withScopes(...) 在作用域定义内部可以工作。

由于 scope 是任意模型类上的静态属性,TypeScript 无法自动推断它接收的是哪个模型的查询构建器。声明一个本地 Builder 别名并标注回调的 query 参数,以获得正确的自动补全和类型检查。

app/models/post.ts
import { DateTime } from 'luxon'
import { scope } from '@adonisjs/lucid/orm'
import { ModelQueryBuilderContract } from '@adonisjs/lucid/types/model'
import { PostsSchema } from '#database/schema'
type Builder = ModelQueryBuilderContract<typeof Post>
export default class Post extends PostsSchema {
static notDeleted = scope((query) => {
query.whereNull('deletedAt')
})
static publishedAndLive = scope((query: Builder) => {
query
.withScopes((scopes) => scopes.notDeleted())
.where('publishedOn', '<=', DateTime.utc().toSQLDate())
})
}

当组合的作用域也接受参数时,在回调内部转换 query 参数。作用域回调签名将 query 类型与其余参数绑定,因此在第一行进行显式转换可保持调用点简洁。

export default class Project extends ProjectsSchema {
static notArchived = scope((query) => {
query.whereNull('archivedAt')
})
static visibleTo = scope((scopeQuery, user: User) => {
const query = scopeQuery as Builder
query
.withScopes((scopes) => scopes.notArchived())
.where('teamId', user.teamId)
})
}

预加载中的作用域

传递给 preload 的回调接收关联模型的查询构建器,它暴露相同的 withScopesapply 方法。重用作用域来约束预加载的关联关系,而无需重复其过滤逻辑。

const users = await User
.query()
.preload('posts', (postsQuery) => {
postsQuery.withScopes((scopes) => scopes.published())
})

这就是你如何使预加载与应用其余部分已为关联模型使用的过滤器保持一致。