HasManyThrough

HasManyThrough

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

  • 声明 hasManyThrough 关联关系并理解其四个关键选项
  • 急切或延迟加载关联行
  • 按关联行的存在性进行过滤
  • 加载关联行的计数和聚合值
  • 理解为什么 hasManyThrough 没有持久化方法

概述

hasManyThrough 关联关系通过一个中间模型到达关联模型。一个国家通过其用户拥有多篇文章;一个组织通过其账户拥有多张发票。

app/models/country.ts
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_idcountries 连接到 users,然后通过 posts.user_idusers 连接到 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 形式。对于主键为 idUser 中间模型,默认值为 Post 上的 userId,由 user_id 列支持。

throughLocalKey

中间模型上关联模型的 throughForeignKey 指向的列。默认为中间模型的主键。

onQuery

Lucid 为该关联关系生成的每个读取查询上运行的回调。

@hasManyThrough([() => Post, () => User], {
onQuery: (query) => query.where('posts.is_published', true),
})
declare posts: HasManyThrough<typeof Post>

preloadrelated('posts').query() 以及 haswhereHaswithCountwithAggregate 使用的子查询上触发。

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)

按关联关系过滤

在父记录的查询构建器上使用 haswhereHas 以基于关联记录的存在性限制行。

// 至少有一篇文章的国家(通过其用户)
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关联关系有匹配行
orHasOR 组合的存在性检查
doesntHave / andDoesntHave关联关系没有匹配行
orDoesntHaveOR 组合的不存在检查
whereHas / andWhereHas关联关系有带约束的匹配行
orWhereHasOR 组合的 whereHas
whereDoesntHave / andWhereDoesntHave关联关系没有带约束的匹配行
orWhereDoesntHaveOR 组合的 whereDoesntHave

聚合

使用 withCountwithAggregate 从关联关系加载派生值。结果存放在父记录的 $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 是只读的。与 hasManymanyToMany 不同,country.related('posts') 上没有 savecreateattachsync

原因是为一个国家创建文章隐式需要一个用户,而 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 集成。