跳至主内容区

多对多关系

非官方测试版翻译

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

什么是多对多关系?

多对多关系是指:实体A包含多个实体B的实例,同时实体B也包含多个实体A的实例。以Question(问题)和Category(分类)实体为例:一个问题可以关联多个分类,而每个分类也可以包含多个问题。

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

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

@Column()
name: string
}
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(() => Category)
@JoinTable()
categories: Category[]
}

@ManyToMany关系中必须使用@JoinTable()装饰器, 且只能将@JoinTable放在关系的拥有方(owning side)。

此示例将生成以下表结构:

+-------------+--------------+----------------------------+
| category |
+-------------+--------------+----------------------------+
| id | int | PRIMARY KEY AUTO_INCREMENT |
| name | varchar(255) | |
+-------------+--------------+----------------------------+

+-------------+--------------+----------------------------+
| question |
+-------------+--------------+----------------------------+
| id | int | PRIMARY KEY AUTO_INCREMENT |
| title | varchar(255) | |
| text | varchar(255) | |
+-------------+--------------+----------------------------+

+-------------+--------------+----------------------------+
| question_categories_category |
+-------------+--------------+----------------------------+
| questionId | int | PRIMARY KEY FOREIGN KEY |
| categoryId | int | PRIMARY KEY FOREIGN KEY |
+-------------+--------------+----------------------------+

保存多对多关系

启用级联操作后, 只需单次save调用即可保存整个关系。

const category1 = new Category()
category1.name = "animals"
await dataSource.manager.save(category1)

const category2 = new Category()
category2.name = "zoo"
await dataSource.manager.save(category2)

const question = new Question()
question.title = "dogs"
question.text = "who let the dogs out?"
question.categories = [category1, category2]
await dataSource.manager.save(question)

删除多对多关系

启用级联操作后, 只需单次save调用即可删除整个关系。

要删除两个记录间的多对多关联, 将其从对应字段中移除后保存记录即可。

const question = await dataSource.getRepository(Question).findOne({
relations: {
categories: true,
},
where: { id: 1 },
})
question.categories = question.categories.filter((category) => {
return category.id !== categoryToRemove.id
})
await dataSource.manager.save(question)

此操作仅删除联结表中的记录, questioncategoryToRemove记录本身仍然存在。

使用级联进行关系软删除

此示例演示级联软删除的行为:

const category1 = new Category()
category1.name = "animals"

const category2 = new Category()
category2.name = "zoo"

const question = new Question()
question.categories = [category1, category2]
const newQuestion = await dataSource.manager.save(question)

await dataSource.manager.softRemove(newQuestion)

本例中我们未对category1和category2调用save或softRemove,但当关系选项的级联设置为true时(包含插入、更新和软删除操作),它们将被自动保存并软删除。

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

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

@ManyToMany(() => Category, (category) => category.questions, {
cascade: true,
})
@JoinTable()
categories: Category[]
}

加载多对多关系

要加载包含分类信息的问题数据, 需在FindOptions中指定关联关系:

const questionRepository = dataSource.getRepository(Question)
const questions = await questionRepository.find({
relations: {
categories: true,
},
})

或使用 QueryBuilder 进行关联查询:

const questions = await dataSource
.getRepository(Question)
.createQueryBuilder("question")
.leftJoinAndSelect("question.categories", "category")
.getMany()

若在关系上启用预加载(eager loading),则无需在查询命令中显式指定关系——系统将自动加载。但使用 QueryBuilder 时预加载功能被禁用,必须通过 leftJoinAndSelect 手动加载关系。

双向关系

关系可分为单向(unidirectional)和双向(bidirectional)。 单向关系仅在单侧设置关系装饰器, 双向关系则在两侧都设置关系装饰器。

前文创建的是单向关系,现将其改为双向:

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

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

@Column()
name: string

@ManyToMany(() => Question, (question) => question.categories)
questions: Question[]
}
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(() => Category, (category) => category.questions)
@JoinTable()
categories: Category[]
}

注意:反向关系不需要@JoinTable装饰器,@JoinTable必须仅存在于关系的单侧。

双向关系支持通过 QueryBuilder 从任意方进行关联查询:

const categoriesWithQuestions = await dataSource
.getRepository(Category)
.createQueryBuilder("category")
.leftJoinAndSelect("category.questions", "question")
.getMany()

带自定义属性的多对多关系

如需在多对多关系中添加额外属性, 需要自行创建新的实体。 例如:若要在QuestionCategory间建立 带order(排序)列的多对多关系, 需创建QuestionToCategory实体, 包含两个ManyToOne关系,分别指向两个实体,以及自定义字段:

import { Entity, Column, ManyToOne, PrimaryGeneratedColumn } from "typeorm"
import { Question } from "./question"
import { Category } from "./category"

@Entity()
export class QuestionToCategory {
@PrimaryGeneratedColumn()
public questionToCategoryId: number

@Column()
public questionId: number

@Column()
public categoryId: number

@Column()
public order: number

@ManyToOne(() => Question, (question) => question.questionToCategories)
public question: Question

@ManyToOne(() => Category, (category) => category.questionToCategories)
public category: Category
}

同时需要在QuestionCategory实体中 添加如下关联关系:

// category.ts
...
@OneToMany(() => QuestionToCategory, questionToCategory => questionToCategory.category)
public questionToCategories: QuestionToCategory[];

// question.ts
...
@OneToMany(() => QuestionToCategory, questionToCategory => questionToCategory.question)
public questionToCategories: QuestionToCategory[];