HasMany
本指南涵盖 hasMany 关联关系。你将学会如何:
- 声明
hasMany关联关系并理解其选项 - 急切或延迟加载关联行
- 限制和排序每个父记录的预加载行
- 按关联行的存在性进行过滤
- 加载关联行的计数和聚合值
- 通过父记录创建、保存和更新关联行
概述
hasMany 关联关系声明另一个模型持有指向你的模型主键的外键,并且每个父记录可以有多条这样的行。一个用户有多篇文章,一篇文章有多条评论,一个项目有多个任务。
import { hasMany } from '@adonisjs/lucid/orm'
import type { HasMany } from '@adonisjs/lucid/types/relations'
import { UsersSchema } from '#database/schema'
import Post from '#models/post'
export default class User extends UsersSchema {
@hasMany(() => Post)
declare posts: HasMany<typeof Post>
}
从被引用侧使用 hasMany。持有外键的模型使用 belongsTo 声明反侧。
参见关联关系简介了解支持此关联关系的迁移以及 Lucid 遵循的约定。
选项
装饰器接受一个 options 对象作为第二个参数。
@hasMany(() => Post, {
foreignKey: 'authorId',
localKey: 'id',
onQuery: (query) => query.whereNull('deleted_at'),
})
declare posts: HasMany<typeof Post>
-
foreignKey
-
关联模型上持有外键值的属性。默认为
{ThisModel}_{primaryKey}的 camelCase 形式。对于User.hasMany(() => Post),默认值为Post上的userId,由user_id列支持。 -
localKey
-
此模型上关联模型的外键指向的列。默认为此模型的主键,几乎总是
id。 -
onQuery
-
Lucid 为该关联关系生成的每个读取查询上运行的回调。在此处附加默认约束,使它们在每次加载或查询关联关系时自动应用。
@hasMany(() => Post, {onQuery: (query) => query.whereNull('deleted_at').orderBy('created_at', 'desc'),})declare posts: HasMany<typeof Post>在
preload、related('posts').query()以及has、whereHas、withCount和withAggregate使用的子查询上触发。不在save、create、createMany、saveMany、firstOrCreate、updateOrCreate、fetchOrCreateMany或updateOrCreateMany上触发,因为它们直接通过外键列写入。 -
meta
-
附加到关联关系定义的任意元数据。Lucid 不读取此字段;它可用于你在运行时检查关联关系定义的工具。
加载关联行
使用 preload 预加载
在查询构建器上调用 preload('posts') 以在每个返回的行上水合关联关系。无论返回了多少个用户,都会运行一条额外查询。
const users = await User.query().preload('posts')
users.forEach((user) => {
console.log(user.posts.length)
})
传入回调以过滤或排序关联关系查询。
await User.query().preload('posts', (postsQuery) => {
postsQuery.where('is_published', true).orderBy('created_at', 'desc')
})
当没有关联行时,user.posts 是一个空数组。
使用 groupLimit 限制每个父记录的行数
preload 回调中的普通 .limit(5) 不会为每个父记录产生五行。它限制所有父记录合计返回的总行数,这几乎从来不是你想要的。
要为每个父记录加载前 N 条,使用 groupLimit 和 groupOrderBy。这些方法在底层使用窗口函数,在应用限制之前按父记录对结果进行分区。
await User.query().preload('posts', (postsQuery) => {
postsQuery.groupLimit(5).groupOrderBy('created_at', 'desc')
})
// 每个用户获得其 5 篇最近的文章,无论返回了多少个用户。
groupLimit 和 groupOrderBy 仅在 preload 内部应用。当你通过 related('posts').query() 延迟加载时,普通的 .limit(...) 和 .orderBy(...) 可以工作,因为查询针对单个父记录运行。
从实例延迟加载
当你已经拥有模型实例且仅在某些代码路径中需要关联行时,通过 related('posts').query() 构建查询。
const user = await User.findOrFail(params.id)
const recentPosts = await user
.related('posts')
.query()
.orderBy('created_at', 'desc')
.limit(5)
按关联关系过滤
在父记录的查询构建器上使用 has 和 whereHas 以基于关联记录的存在性限制行。hasMany 支持带有运算符和值的基于计数的过滤。
// 至少有一篇文章的用户
const authors = await User.query().has('posts')
// 有五篇或更多文章的用户
const prolific = await User.query().has('posts', '>=', 5)
// 最近 30 天内至少有一篇已发布文章的用户
const recent = await User.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 users = await User.query().withCount('posts')
users.forEach((user) => {
console.log(user.$extras.posts_count)
})
默认别名是 {relationName}_count。通过回调覆盖它。
const users = await User
.query()
.withCount('posts', (query) => {
query.where('is_published', true).as('publishedPostsCount')
})
withAggregate 运行任何聚合函数。在回调中使用 .as(...) 定义别名。
const users = await User
.query()
.withAggregate('posts', (query) => {
query.max('created_at').as('lastPostAt')
})
通过关联关系持久化
以下每个方法都在托管事务中运行。父记录首先被保存以便其主键可用,关联行的外键被自动设置,然后保存关联行。如果任何操作失败,整批操作回滚。
save 和 saveMany
save(related) 将单个未保存的模型实例作为父记录的子记录持久化。saveMany(related[]) 对数组做同样的事。
const user = await User.findOrFail(1)
const post = new Post()
post.title = 'Hello'
post.body = 'World'
await user.related('posts').save(post)
// post.userId 现在为 user.id,且 post 行已持久化
const drafts = [new Post(), new Post(), new Post()]
// 在每个上赋值 title/body
await user.related('posts').saveMany(drafts)
create 和 createMany
create(values) 从值构建模型实例,从父记录设置外键,并持久化。createMany(values[]) 对数组做同样的事。
const post = await user.related('posts').create({
title: 'Hello',
body: 'World',
})
const posts = await user.related('posts').createMany([
{ title: 'First', body: '...' },
{ title: 'Second', body: '...' },
])
firstOrCreate
在关联关系中搜索匹配搜索载荷的行。没有匹配时创建一个。保存载荷(如果提供)在创建时与搜索载荷合并,在行已存在时被忽略。
const post = await user.related('posts').firstOrCreate(
{ slug: 'hello-world' }, // 搜索
{ title: 'Hello, world', body: '' } // 仅在创建时使用
)
updateOrCreate
用更新载荷更新匹配的行,或在没有匹配时用合并的载荷创建新行。
await user.related('posts').updateOrCreate(
{ slug: 'hello-world' },
{ title: 'Updated title', body: 'Updated body' }
)
fetchOrCreateMany 和 updateOrCreateMany
与其单行对应方法工作方式相同的批量变体。两者都接受要同步的行数组,加上标识每行的谓词键(或键数组)。
// 确保每个 slug 都存在一个标签,插入缺失的
await project.related('tags').fetchOrCreateMany([
{ slug: 'alpha', label: 'Alpha' },
{ slug: 'beta', label: 'Beta' },
], 'slug'),
// 更新现有标签上的 label 或插入新行
await project.related('tags').updateOrCreateMany([
{ slug: 'alpha', label: 'Alpha (updated)' },
{ slug: 'beta', label: 'Beta (updated)' },
], 'slug')
对于两种批量变体,谓词都会自动与关联关系的外键组合。你无需在载荷或谓词中包含外键;Lucid 从父记录设置它。
分页
当你通过 related('posts').query() 延迟加载关联关系时,paginate(page, perPage) 可以工作。
const user = await User.findOrFail(params.id)
const posts = await user
.related('posts')
.query()
.orderBy('created_at', 'desc')
.paginate(page, 20)
paginate 不允许在 preload 回调内部使用。在一个查询中跨多个父记录分页没有明确定义的结构,因为每个父记录需要自己的页面边界。解决方法是先查询父记录,然后为你的端点需要的特定父记录延迟加载分页的关联关系。
参见分页指南了解分页器 API、URL 自定义和 transformer 集成。