HasOne
本指南涵盖 hasOne 关联关系。你将学会如何:
- 声明
hasOne关联关系并理解其选项 - 急切或延迟加载关联行
- 按关联行的存在性进行过滤
- 通过
save、create、firstOrCreate和updateOrCreate持久化关联行
概述
hasOne 关联关系声明另一个模型持有指向你的模型主键的外键,并且每个父记录最多只有一行。一个用户有一个个人资料,一个组织有一个计费配置,一个团队有一个所有者。
import { hasOne } from '@adonisjs/lucid/orm'
import type { HasOne } from '@adonisjs/lucid/types/relations'
import { UsersSchema } from '#database/schema'
import Profile from '#models/profile'
export default class User extends UsersSchema {
@hasOne(() => Profile)
declare profile: HasOne<typeof Profile>
}
从被引用侧使用 hasOne。持有外键的模型使用 belongsTo 声明反侧。
"每个父记录一行"的保证必须由数据库强制执行。在关联模型的迁移中为外键列添加唯一索引,以便数据库拒绝创建第二个关联行的尝试。参见关联关系简介了解迁移结构。
选项
装饰器接受一个 options 对象作为第二个参数。
@hasOne(() => Profile, {
foreignKey: 'userId',
localKey: 'id',
onQuery: (query) => query.whereNull('deleted_at'),
})
declare profile: HasOne<typeof Profile>
-
foreignKey
-
关联模型上持有外键值的属性。默认为
{ThisModel}_{primaryKey}的 camelCase 形式。对于User.hasOne(() => Profile),默认值为Profile上的userId,由user_id列支持。 -
localKey
-
此模型上关联模型的外键指向的列。默认为此模型的主键,几乎总是
id。 -
onQuery
-
Lucid 为该关联关系生成的每个读取查询上运行的回调。
@hasOne(() => Profile, {onQuery: (query) => query.whereNull('deleted_at'),})declare profile: HasOne<typeof Profile>在
preload、related('profile').query()以及has、whereHas、withCount和withAggregate使用的子查询上触发。不在save、create、firstOrCreate或updateOrCreate上触发,因为它们直接通过外键列写入。 -
meta
-
附加到关联关系定义的任意元数据。Lucid 不读取此字段;它可用于你在运行时检查关联关系定义的工具。
加载关联行
使用 preload 预加载
在查询构建器上调用 preload('profile') 以在每个返回的行上水合关联关系。无论返回了多少个用户,都会运行一条额外查询。
const users = await User.query().preload('profile')
users.forEach((user) => {
console.log(user.profile?.displayName)
})
传入回调以过滤或选择关联关系查询。
await User.query().preload('profile', (profileQuery) => {
profileQuery.select('id', 'user_id', 'display_name', 'avatar_url')
})
当没有关联行时,user.profile 为 null。在访问字段前使用可选链或显式检查进行保护。
从实例延迟加载
当你已经拥有模型实例且仅在某些代码路径中需要关联行时,通过 related('profile').query() 构建查询。
const user = await User.findOrFail(params.id)
const profile = await user.related('profile').query().first()
按关联关系过滤
在父记录的查询构建器上使用 has 和 whereHas 以基于关联记录的存在性限制行。
// 有个人资料行的用户
const usersWithProfile = await User.query().has('profile')
// 个人资料标记为公开的用户
const publicProfiles = await User.query().whereHas('profile', (profileQuery) => {
profileQuery.where('is_public', true)
})
组合和取反的变体:
| 方法 | 描述 |
|---|---|
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('profile')
users.forEach((user) => {
// 存在个人资料时为 1,否则为 0
console.log(user.$extras.profile_count)
})
由于 hasOne 每个父记录最多返回一行,withCount 主要用作与该行的其他数据共存的存在性标志。对于更丰富的投影,使用 withAggregate。
const users = await User
.query()
.withAggregate('profile', (query) => {
query.max('updated_at').as('profileUpdatedAt')
})
通过关联关系持久化
以下每个方法都在托管事务中运行。父记录首先被保存以便其主键可用,关联行的外键被自动设置,然后保存关联行。如果任何操作失败,两次写入都会回滚。
save
save(related) 将关联模型实例作为父记录的子记录持久化。
const user = await User.findOrFail(1)
const profile = new Profile()
profile.displayName = 'Harminder'
profile.bio = 'Building AdonisJS'
await user.related('profile').save(profile)
// profile.userId === user.id 且行已持久化
当数据库不强制执行单行约束时,save 不会阻止创建第二个个人资料。始终在关联表的迁移中为外键列添加唯一索引,以便数据库拒绝重复项。
create
create(values) 从值构建新的关联实例,从父记录设置外键,并持久化。
const profile = await user.related('profile').create({
displayName: 'Harminder',
bio: 'Building AdonisJS',
})
firstOrCreate
在关联关系中搜索匹配搜索载荷的行。没有匹配时创建一个。保存载荷(如果提供)在创建时与搜索载荷合并,在行已存在时被忽略。
const profile = await user.related('profile').firstOrCreate(
{}, // 搜索(空:匹配此用户的任何个人资料)
{ displayName: 'New user', bio: '' } // 仅在创建时使用
)
firstOrCreate 是确保 hasOne 行存在而不会冒重复风险的幂等方式。从处理"如果缺失则创建个人资料"流程的控制器中调用它。
updateOrCreate
用更新载荷更新匹配的行,或在没有匹配时用合并的载荷创建新行。
await user.related('profile').updateOrCreate(
{}, // 搜索
{ displayName: 'Harminder', bio: 'Updated' } // 在两条路径上都应用
)
分页
hasOne 关联关系不能分页。关联关系每个父记录最多解析为一行,因此 paginate 无法生成有意义的结构。在运行时调用 user.related('profile').query().paginate(...) 会抛出异常。
要对 hasOne 的父记录侧进行分页,照常使用 User.query().paginate(...) 并在每行上预加载 profile。