BelongsTo
本指南涵盖 belongsTo 关联关系。你将学会如何:
- 声明
belongsTo关联关系并理解其选项 - 急切或延迟加载关联行
- 按关联关系的存在性进行过滤
- 加载关联关系的计数和聚合值
- 在运行时关联和解关联父记录
概述
belongsTo 关联关系声明你的模型持有一个指向另一模型主键的外键列。每一行要么精确指向一个父记录,要么外键设置为 NULL。
import { belongsTo } from '@adonisjs/lucid/orm'
import type { BelongsTo } from '@adonisjs/lucid/types/relations'
import { PostsSchema } from '#database/schema'
import User from '#models/user'
export default class Post extends PostsSchema {
@belongsTo(() => User)
declare author: BelongsTo<typeof User>
}
当你的模型是持有外键的一侧时,使用 belongsTo。属于某个作者的文章、属于某个客户的订单、属于某篇文章和某个用户的评论都符合此模式。
参见关联关系简介了解支持此关联关系的迁移以及 Lucid 遵循的约定。
选项
装饰器接受一个 options 对象作为第二个参数。
@belongsTo(() => User, {
foreignKey: 'authorId',
localKey: 'id',
onQuery: (query) => query.whereNull('deleted_at'),
})
declare author: BelongsTo<typeof User>
-
foreignKey
-
此模型上持有外键值的属性。默认为
{RelatedModel}_{primaryKey}的 camelCase 形式。对于belongsTo(() => User),默认值为userId。 -
localKey
-
关联模型上外键指向的列。默认为关联模型的主键,几乎总是
id。 -
onQuery
-
Lucid 为该关联关系生成的每个读取查询上运行的回调。在此处附加默认约束,使它们在每次加载或查询关联关系时自动应用。
@belongsTo(() => User, {onQuery: (query) => query.whereNull('deactivated_at'),})declare author: BelongsTo<typeof User>在
preload、related('author').query()以及has、whereHas、withCount和withAggregate使用的子查询上触发。不在associate或dissociate上触发,因为它们直接更新外键而不运行读取查询。 -
meta
-
附加到关联关系定义的任意元数据。Lucid 不读取此字段;它可用于你在运行时检查关联关系定义的工具。
加载关联模型
使用 preload 预加载
在查询构建器上调用 preload('author') 以在每个返回的行上水合关联关系。无论返回了多少篇文章,都会运行一条额外查询。
const posts = await Post.query().preload('author').orderBy('created_at', 'desc')
posts.forEach((post) => {
console.log(post.author.email)
})
传入回调以过滤或排序关联关系查询。
await Post.query().preload('author', (authorsQuery) => {
authorsQuery.select('id', 'email', 'first_name', 'last_name')
})
当外键可为空且该行没有作者时,预加载后 post.author 为 null。
从实例延迟加载
当你已经拥有模型实例且仅在某些代码路径中需要关联行时,通过 related('author').query() 构建查询。
const post = await Post.findOrFail(params.id)
if (someCondition) {
const author = await post.related('author').query().firstOrFail()
}
按关联关系过滤
在父记录的查询构建器上使用 has 和 whereHas 以基于关联记录的存在性限制行。belongsTo 两者都支持,但最常见的情况是检查外键是否实际解析为一行。
// 其作者仍然存在于 users 表中的文章
const posts = await Post.query().has('author')
// 其作者的订阅处于活跃状态的文章
const posts = await Post
.query()
.whereHas('author', (authorQuery) => {
authorQuery.where('subscription_status', 'active')
})
组合和取反的变体:
| 方法 | 描述 |
|---|---|
has / andHas | 关联关系有匹配行 |
orHas | OR 组合的存在性检查 |
doesntHave / andDoesntHave | 关联关系没有匹配行 |
orDoesntHave | OR 组合的不存在检查 |
whereHas / andWhereHas | 关联关系有带约束的匹配行 |
orWhereHas | OR 组合的 whereHas |
whereDoesntHave / andWhereDoesntHave | 关联关系没有带约束的匹配行 |
orWhereDoesntHave | OR 组合的 whereDoesntHave |
聚合
使用 withCount 和 withAggregate 从关联关系加载派生值而不加载行本身。结果存放在父记录的 $extras 对象上。
const posts = await Post.query().withCount('author')
posts.forEach((post) => {
// 作者存在时为 1,外键为 null 或行不存在时为 0
console.log(post.$extras.author_count)
})
由于 belongsTo 每个父记录最多返回一行,withCount('author') 主要用作与该行的其他数据共存的存在性检查。通过 withAggregate 的自定义聚合遵循相同的模式。
const posts = await Post
.query()
.withAggregate('author', (query) => {
query.max('last_login_at').as('authorLastLoginAt')
})
关联和解关联
associate(related)
associate 将关联实例链接到父记录。Lucid 保存关联模型(以便填充其主键),将父记录的外键设置为关联模型的主键,并保存父记录。两次写入都在托管事务中运行,因此要么两者都提交,要么两者都回滚。
const post = new Post()
post.title = 'Hello'
post.body = 'World'
const user = await User.findOrFail(authorId)
await post.related('author').associate(user)
// post 现在已持久化,且 post.authorId === user.id
调用 associate 前关联模型不需要已持久化。Lucid 会在事务内保存它。
dissociate()
dissociate 将父记录的外键列设置为 null 并保存父记录。外键列必须可为空才能在数据库级别生效。
const post = await Post.findOrFail(params.id)
await post.related('author').dissociate()
// post.authorId 现在为 null;用户行仍然存在
dissociate 从不删除关联行。它仅通过置空外键来断开链接。
分页
belongsTo 关联关系不能分页。关联关系每个父记录最多解析为一行,因此 paginate 无法生成有意义的结构。在运行时调用 post.related('author').query().paginate(...) 会抛出异常。
要对 belongsTo 的父记录侧进行分页,照常使用 Post.query().paginate(...) 并在每行上预加载 author。