跳至主内容区

关系操作常见问题解答

非官方测试版翻译

本页面由 PageTurner AI 翻译(测试版)。未经项目官方认可。 发现错误? 报告问题 →

如何创建自引用关系?

自引用关系是指实体与自身建立的关系。 这种关系在存储树形结构实体时非常有用, "邻接表"模式就是通过自引用关系实现的。 例如,您希望在应用中创建分类树: 分类可以嵌套子分类,子分类又能继续嵌套其他分类等。 自引用关系在此场景下非常便利。 本质上,自引用关系就是指向实体自身的常规关系。 示例:

import {
Entity,
PrimaryGeneratedColumn,
Column,
ManyToOne,
OneToMany,
} from "typeorm"

@Entity()
export class Category {
@PrimaryGeneratedColumn()
id: number

@Column()
title: string

@Column()
text: string

@ManyToOne((type) => Category, (category) => category.childCategories)
parentCategory: Category

@OneToMany((type) => Category, (category) => category.parentCategory)
childCategories: Category[]
}

如何在不加载关系的情况下获取关联ID?

有时您希望直接获取关联对象的ID而不加载完整对象。 例如:

import { Entity, PrimaryGeneratedColumn, Column } from "typeorm"

@Entity()
export class Profile {
@PrimaryGeneratedColumn()
id: number

@Column()
gender: string

@Column()
photo: string
}
import {
Entity,
PrimaryGeneratedColumn,
Column,
OneToOne,
JoinColumn,
} from "typeorm"
import { Profile } from "./Profile"

@Entity()
export class User {
@PrimaryGeneratedColumn()
id: number

@Column()
name: string

@OneToOne((type) => Profile)
@JoinColumn()
profile: Profile
}

当您加载用户数据时若未关联profile, 用户对象中将不包含任何档案信息, 甚至不会包含档案ID:

User {
id: 1,
name: "Umed"
}

但有时您需要获取用户的"档案ID"而不加载完整档案。 只需在实体中添加与关系字段同名的@Column属性即可:

import {
Entity,
PrimaryGeneratedColumn,
Column,
OneToOne,
JoinColumn,
} from "typeorm"
import { Profile } from "./Profile"

@Entity()
export class User {
@PrimaryGeneratedColumn()
id: number

@Column()
name: string

@Column({ nullable: true })
profileId: number

@OneToOne((type) => Profile)
@JoinColumn()
profile: Profile
}

操作完成后,下次加载的用户对象将包含档案ID:

User {
id: 1,
name: "Umed",
profileId: 1
}

如何在实体中加载关联关系?

最简单的加载方式是使用FindOptions中的relations选项:

const users = await dataSource.getRepository(User).find({
relations: {
profile: true,
photos: true,
videos: true,
},
})

另一种更灵活的方法是使用QueryBuilder

const user = await dataSource
.getRepository(User)
.createQueryBuilder("user")
.leftJoinAndSelect("user.profile", "profile")
.leftJoinAndSelect("user.photos", "photo")
.leftJoinAndSelect("user.videos", "video")
.getMany()

使用QueryBuilder时:

  • 可用innerJoinAndSelect替代leftJoinAndSelect (了解LEFT JOININNER JOIN区别请参考SQL文档)
  • 可基于条件加载关联数据
  • 支持排序等高级操作

详细了解QueryBuilder

避免初始化关系属性

有时初始化关系属性看似合理,例如:

import {
Entity,
PrimaryGeneratedColumn,
Column,
ManyToMany,
JoinTable,
} from "typeorm"
import { Category } from "./Category"

@Entity()
export class Question {
@PrimaryGeneratedColumn()
id: number

@Column()
title: string

@Column()
text: string

@ManyToMany((type) => Category, (category) => category.questions)
@JoinTable()
categories: Category[] = [] // see = [] initialization here
}

但在TypeORM实体中可能导致问题。 为理解该问题,先观察未初始化的Question实体加载效果:

Question {
id: 1,
title: "Question about ..."
}

保存此对象时,其内部categories不会变动——因为未初始化。

但若已初始化,加载的实体将变为:

Question {
id: 1,
title: "Question about ...",
categories: []
}

保存此对象时,系统会检查已绑定该问题的分类, 并解除所有关联。原因在于:

  • 空数组[]会被认为移除了所有关联项
  • 系统无法区分"主动清空"和"未加载关联"

因此保存此类对象将导致问题——它会清除所有现有分类关联。

如何避免?请勿在实体中初始化数组。 该规则同样适用于构造函数——避免在构造函数内初始化。

避免创建外键约束

出于性能考虑,您可能希望建立实体关系但不创建外键约束。 可通过createForeignKeyConstraints选项控制(默认值:true):

import { Entity, PrimaryColumn, Column, ManyToOne } from "typeorm"
import { Person } from "./Person"

@Entity()
export class ActionLog {
@PrimaryColumn()
id: number

@Column()
date: Date

@Column()
action: string

@ManyToOne((type) => Person, {
createForeignKeyConstraints: false,
})
person: Person
}

避免循环导入错误

当多个实体文件相互引用时(例如多对多关系), 可通过以下方式避免环境中的循环导入错误。 假设Action.ts和Person.ts需要相互引用: 我们使用import type仅导入类型信息, 避免生成实际JavaScript代码。

import { Entity, PrimaryColumn, Column, ManytoMany } from "typeorm"
import type { Person } from "./Person"

@Entity()
export class ActionLog {
@PrimaryColumn()
id: number

@Column()
date: Date

@Column()
action: string

@ManyToMany("Person", (person: Person) => person.id)
person: Person
}
import { Entity, PrimaryColumn, ManytoMany } from "typeorm"
import type { ActionLog } from "./Action"

@Entity()
export class Person {
@PrimaryColumn()
id: number

@ManyToMany("ActionLog", (actionLog: ActionLog) => actionLog.id)
log: ActionLog
}