简介

关联关系

本指南是 Lucid 关联关系的概念性入口。你将了解:

  • Lucid 中的关联关系是什么以及如何声明
  • Lucid 如何命名关联关系以及它们在代码中的位置
  • 可用的关联关系类型,包括每种类型的数据库布局和模型代码

每种关联关系类型的详细加载、查询和写入 API 位于页面底部链接的各关联关系指南中。

概述

关联关系通过共享列将一个模型连接到另一个模型。你在模型类上声明一次关联关系,然后在代码中遍历它们而无需手动编写连接。

app/models/user.ts
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>
}
app/models/post.ts
import { belongsTo } from '@adonisjs/lucid/orm'
import type { BelongsTo } from '@adonisjs/lucid/types/relations'
import { PostsSchema } from '#database/schema'
import User from '#models/user'
export default class Post extends PostsSchema {
@belongsTo(() => User)
declare author: BelongsTo<typeof User>
}

现在两侧已连接。你可以从任一方向遍历关联关系。

const user = await User.query().preload('posts').firstOrFail()
console.log(user.posts)
const post = await Post.query().preload('author').firstOrFail()
console.log(post.author.email)

Lucid 如何表达关联关系

一些约定适用于你声明的每个关联关系。

关联关系存在于模型上

关联关系在 app/models/ 内的模型类中声明。它们不是生成的 schema 类的一部分。重新生成 database/schema.ts 永远不会触及你的关联关系。

方向性命名

关联关系根据哪一侧持有外键来命名,而不是根据行数基数。

  • 你的模型持有外键。使用 belongsTo
  • 另一个模型的外键指向你,且最多只有一行。使用 hasOne
  • 另一个模型的外键指向你,且可以有多行。使用 hasMany
  • 两个模型共享一个持有双方外键的中间表。使用 manyToMany
  • 你通过中间模型到达目标模型。使用 hasManyThrough

这种方向性风格从两侧读起来都一致。Post 属于一个 UserUser 有多个 Post。外键位于何处没有歧义。

基于函数的关联模型引用

关联模型作为返回类的函数传入。

@hasMany(() => Post)
declare posts: HasMany<typeof Post>

该函数将 Post 的解析延迟到每个模块加载之后,这避免了两个模型相互引用时的循环导入错误。

覆盖位于装饰器上

当 Lucid 的默认值与你的 schema 不匹配时,每个装饰器都接受一个 options 对象用于关键覆盖。

@belongsTo(() => User, { foreignKey: 'authorId' })
declare author: BelongsTo<typeof User>

每个装饰器上的显式覆盖是自定义键的规范方式。它们是可搜索的,并使每个关联关系自文档化。

关联关系类型

每种可用关联关系类型的迁移和模型结构。

BelongsTo

一篇文章属于一个用户。拥有模型持有一个指向关联模型主键的外键列。

database/migrations/xxxx_create_posts_table.ts
import { BaseSchema } from '@adonisjs/lucid/schema'
export default class extends BaseSchema {
async up() {
this.schema.createTable('posts', (table) => {
table.increments('id')
table.string('title').notNullable()
table.text('body')
table
.integer('user_id')
.unsigned()
.notNullable()
.references('users.id')
.onDelete('CASCADE')
table.timestamps(true, true)
})
}
}
app/models/post.ts
import { belongsTo } from '@adonisjs/lucid/orm'
import type { BelongsTo } from '@adonisjs/lucid/types/relations'
import { PostsSchema } from '#database/schema'
import User from '#models/user'
export default class Post extends PostsSchema {
@belongsTo(() => User)
declare author: BelongsTo<typeof User>
}

HasOne

一个用户有一个个人资料。关联模型持有一个指向父记录主键的外键列,唯一索引确保每个父记录只有一行。

database/migrations/xxxx_create_profiles_table.ts
import { BaseSchema } from '@adonisjs/lucid/schema'
export default class extends BaseSchema {
async up() {
this.schema.createTable('profiles', (table) => {
table.increments('id')
table
.integer('user_id')
.unsigned()
.notNullable()
.unique()
.references('users.id')
.onDelete('CASCADE')
table.string('display_name')
table.text('bio')
table.timestamps(true, true)
})
}
}
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>
}

HasMany

一个用户有多篇文章。关联模型持有一个指向父记录主键的外键列,没有唯一性约束。

database/migrations/xxxx_create_posts_table.ts
import { BaseSchema } from '@adonisjs/lucid/schema'
export default class extends BaseSchema {
async up() {
this.schema.createTable('posts', (table) => {
table.increments('id')
table.string('title').notNullable()
table.text('body')
table
.integer('user_id')
.unsigned()
.notNullable()
.references('users.id')
.onDelete('CASCADE')
table.timestamps(true, true)
})
}
}
app/models/user.ts
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>
}

ManyToMany

一个用户有多个技能,每个技能属于多个用户。一个中间表持有双方的外键。两个模型都不直接持有对方的外键。

database/migrations/xxxx_create_user_skills_table.ts
import { BaseSchema } from '@adonisjs/lucid/schema'
export default class extends BaseSchema {
async up() {
this.schema.createTable('user_skills', (table) => {
table.increments('id')
table.integer('user_id').unsigned().notNullable().references('users.id').onDelete('CASCADE')
table.integer('skill_id').unsigned().notNullable().references('skills.id').onDelete('CASCADE')
table.unique(['user_id', 'skill_id'])
table.timestamps(true, true)
})
}
}
app/models/user.ts
import { manyToMany } from '@adonisjs/lucid/orm'
import type { ManyToMany } from '@adonisjs/lucid/types/relations'
import { UsersSchema } from '#database/schema'
import Skill from '#models/skill'
export default class User extends UsersSchema {
@manyToMany(() => Skill)
declare skills: ManyToMany<typeof Skill>
}

HasManyThrough

一个国家通过其用户有多篇文章。中间模型持有指向父记录的外键,关联模型持有指向中间模型的外键。

database/migrations/xxxx_create_users_table.ts (excerpt)
table
.integer('country_id')
.unsigned()
.notNullable()
.references('countries.id')
.onDelete('CASCADE')
database/migrations/xxxx_create_posts_table.ts (excerpt)
table
.integer('user_id')
.unsigned()
.notNullable()
.references('users.id')
.onDelete('CASCADE')
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>
}

后续步骤

每种关联关系类型都有自己的指南,涵盖加载、查询、持久化以及完整的选项参考,包括外键覆盖和 onQuery 钩子。

  • BelongsTo 用于一对一和多对一关联关系的拥有侧
  • HasMany 用于从被引用侧的一对多
  • ManyToMany 用于基于中间表的多对多,包括中间表列、时间戳以及 attach、detach 和 sync API
  • HasOne 用于从被引用侧的一对一
  • HasManyThrough 用于遍历中间模型