HasManyThrough
本指南涵盖 hasManyThrough 关联关系。你将学会如何:
- 声明
hasManyThrough关联关系并理解其四个关键选项 - 急切或延迟加载关联行
- 按关联行的存在性进行过滤
- 加载关联行的计数和聚合值
- 理解为什么
hasManyThrough没有持久化方法
概述
hasManyThrough 关联关系通过一个中间模型到达关联模型。一个国家通过其用户拥有多篇文章;一个组织通过其账户拥有多张发票。
import { hasManyThrough } from '@adonisjs/lucid/orm'
import type { HasManyThrough } from '@adonisjs/lucid/types/relations'
import { CountriesSchema } from '#database/schema'
import Post from '#models/post'
import User from '#models/user'
export default class Country extends CountriesSchema {
@hasManyThrough([() => Post, () => User])
declare posts: HasManyThrough<typeof Post>
}
装饰器接受两个模型引用的元组。第一个元素是你想要到达的目标模型;第二个是 Lucid 遍历的中间模型。在上面的例子中,Lucid 通过 users.country_id 将 countries 连接到 users,然后通过 posts.user_id 将 users 连接到 posts。
参见关联关系简介了解支持此关联关系的迁移结构。
选项
装饰器接受一个 options 对象作为第二个参数。四个键描述了三个表之间的链。
@hasManyThrough([() => Post, () => User], {
foreignKey: 'countryId', // users.country_id 指向 countries.id
localKey: 'id', // countries.id
throughForeignKey: 'userId', // posts.user_id 指向 users.id
throughLocalKey: 'id', // users.id
})
declare posts: HasManyThrough<typeof Post>
-
foreignKey
-
中间模型上持有指向此模型外键的属性。默认为
{ThisModel}_{primaryKey}的 camelCase 形式。对于Country.hasManyThrough([Post, User]),默认值为User上的countryId,由country_id列支持。 -
localKey
-
此模型上中间模型的
foreignKey指向的列。默认为此模型的主键。 -
throughForeignKey
-
关联模型上持有指向中间模型外键的属性。默认为
{ThroughModel}_{primaryKey}的 camelCase 形式。对于主键为id的User中间模型,默认值为Post上的userId,由user_id列支持。 -
throughLocalKey
-
中间模型上关联模型的
throughForeignKey指向的列。默认为中间模型的主键。 -
onQuery
-
Lucid 为该关联关系生成的每个读取查询上运行的回调。
@hasManyThrough([() => Post, () => User], {onQuery: (query) => query.where('posts.is_published', true),})declare posts: HasManyThrough<typeof Post>在
preload、related('posts').query()以及has、whereHas、withCount和withAggregate使用的子查询上触发。 -
meta
-
附加到关联关系定义的任意元数据。Lucid 不读取此字段;它可用于你在运行时检查关联关系定义的工具。
加载关联行
使用 preload 预加载
在查询构建器上调用 preload('posts') 以在每个返回的国家上水合关联关系。无论返回了多少个国家,都会运行一条额外查询,即使查询通过中间 users 表进行连接。
const countries = await Country.query().preload('posts')
countries.forEach((country) => {
console.log(country.posts.length)
})
传入回调以过滤或排序关联关系查询。回调接收关联模型的查询构建器,因此过滤和排序针对 Post 列。
await Country.query().preload('posts', (postsQuery) => {
postsQuery.where('is_published', true).orderBy('created_at', 'desc')
})
当没有关联行时,country.posts 是一个空数组。
从实例延迟加载
当你已经拥有父实例时,通过 related('posts').query() 构建查询。
const country = await Country.findOrFail(params.id)
const recentPosts = await country
.related('posts')
.query()
.orderBy('created_at', 'desc')
.limit(10)
按关联关系过滤
在父记录的查询构建器上使用 has 和 whereHas 以基于关联记录的存在性限制行。
// 至少有一篇文章的国家(通过其用户)
const active = await Country.query().has('posts')
// 最近 30 天内至少有一篇已发布文章的国家
const recent = await Country.query().whereHas('posts', (postsQuery) => {
postsQuery
.where('is_published', true)
.where('created_at', '>', DateTime.now().minus({ days: 30 }).toSQL())
})
组合和取反的变体:
| 方法 | 描述 |
|---|---|
has / andHas | 关联关系有匹配行 |
orHas | OR 组合的存在性检查 |
doesntHave / andDoesntHave | 关联关系没有匹配行 |
orDoesntHave | OR 组合的不存在检查 |
whereHas / andWhereHas | 关联关系有带约束的匹配行 |
orWhereHas | OR 组合的 whereHas |
whereDoesntHave / andWhereDoesntHave | 关联关系没有带约束的匹配行 |
orWhereDoesntHave | OR 组合的 whereDoesntHave |
聚合
使用 withCount 和 withAggregate 从关联关系加载派生值。结果存放在父记录的 $extras 对象上。
const countries = await Country.query().withCount('posts')
countries.forEach((country) => {
console.log(country.$extras.posts_count)
})
当默认的 {relation}_count 不是你想要的时,通过回调覆盖别名。
const countries = await Country
.query()
.withCount('posts', (query) => {
query.where('is_published', true).as('publishedPostsCount')
})
withAggregate 运行任何聚合函数。
const countries = await Country
.query()
.withAggregate('posts', (query) => {
query.max('created_at').as('latestPostAt')
})
hasManyThrough 不可持久化
hasManyThrough 是只读的。与 hasMany 或 manyToMany 不同,country.related('posts') 上没有 save、create、attach 或 sync。
原因是为一个国家创建文章隐式需要一个用户,而 Lucid 无法推断新文章应该属于哪个用户。直接通过中间关联关系进行持久化。
// 错误:hasManyThrough 没有写入方法
// country.related('posts').create({ ... })
// 正确:通过中间关联关系持久化
const user = await country.related('users').query().firstOrFail()
await user.related('posts').create({ title: 'Hello' })
分页
当你通过 related('posts').query() 延迟加载关联关系时,paginate(page, perPage) 可以工作。
const country = await Country.findOrFail(params.id)
const posts = await country
.related('posts')
.query()
.orderBy('created_at', 'desc')
.paginate(page, 20)
在 preload 回调内分页会抛出异常,因为单个分页器无法表达每个父记录的页面边界。先查询父记录,然后为你的端点需要的特定父记录延迟加载分页的关联关系。
参见分页指南了解分页器 API、URL 自定义和 transformer 集成。