BelongsTo

BelongsTo

本指南涵盖 belongsTo 关联关系。你将学会如何:

  • 声明 belongsTo 关联关系并理解其选项
  • 急切或延迟加载关联行
  • 按关联关系的存在性进行过滤
  • 加载关联关系的计数和聚合值
  • 在运行时关联和解关联父记录

概述

belongsTo 关联关系声明你的模型持有一个指向另一模型主键的外键列。每一行要么精确指向一个父记录,要么外键设置为 NULL

app/models/post.ts
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>

preloadrelated('author').query() 以及 haswhereHaswithCountwithAggregate 使用的子查询上触发。不在 associatedissociate 上触发,因为它们直接更新外键而不运行读取查询。

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.authornull

从实例延迟加载

当你已经拥有模型实例且仅在某些代码路径中需要关联行时,通过 related('author').query() 构建查询。

const post = await Post.findOrFail(params.id)
if (someCondition) {
const author = await post.related('author').query().firstOrFail()
}

按关联关系过滤

在父记录的查询构建器上使用 haswhereHas 以基于关联记录的存在性限制行。belongsTo 两者都支持,但最常见的情况是检查外键是否实际解析为一行。

// 其作者仍然存在于 users 表中的文章
const posts = await Post.query().has('author')
// 其作者的订阅处于活跃状态的文章
const posts = await Post
.query()
.whereHas('author', (authorQuery) => {
authorQuery.where('subscription_status', 'active')
})

组合和取反的变体:

方法描述
has / andHas关联关系有匹配行
orHasOR 组合的存在性检查
doesntHave / andDoesntHave关联关系没有匹配行
orDoesntHaveOR 组合的不存在检查
whereHas / andWhereHas关联关系有带约束的匹配行
orWhereHasOR 组合的 whereHas
whereDoesntHave / andWhereDoesntHave关联关系没有带约束的匹配行
orWhereDoesntHaveOR 组合的 whereDoesntHave

聚合

使用 withCountwithAggregate 从关联关系加载派生值而不加载行本身。结果存放在父记录的 $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