HasOne

HasOne

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

  • 声明 hasOne 关联关系并理解其选项
  • 急切或延迟加载关联行
  • 按关联行的存在性进行过滤
  • 通过 savecreatefirstOrCreateupdateOrCreate 持久化关联行

概述

hasOne 关联关系声明另一个模型持有指向你的模型主键的外键,并且每个父记录最多只有一行。一个用户有一个个人资料,一个组织有一个计费配置,一个团队有一个所有者。

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

preloadrelated('profile').query() 以及 haswhereHaswithCountwithAggregate 使用的子查询上触发。不在 savecreatefirstOrCreateupdateOrCreate 上触发,因为它们直接通过外键列写入。

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.profilenull。在访问字段前使用可选链或显式检查进行保护。

从实例延迟加载

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

const user = await User.findOrFail(params.id)
const profile = await user.related('profile').query().first()

按关联关系过滤

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

// 有个人资料行的用户
const usersWithProfile = await User.query().has('profile')
// 个人资料标记为公开的用户
const publicProfiles = await User.query().whereHas('profile', (profileQuery) => {
profileQuery.where('is_public', true)
})

组合和取反的变体:

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

聚合

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