Перейти к основному содержанию

Отношения «многие ко многим»

Неофициальный Бета-перевод

Эта страница переведена 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 должен указываться только на одной (владеющей) стороне отношения.

Данный пример создаст следующие таблицы:

+-------------+--------------+----------------------------+
| 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)

Это удалит только запись в соединительной таблице. Сами записи question и categoryToRemove останутся.

Мягкое удаление отношений с каскадированием

Пример демонстрирует поведение каскадного мягкого удаления:

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)

Здесь мы не вызывали save или softRemove для category1 и category2, но они будут автоматически сохранены и мягко удалены, когда для параметра каскада установлено 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) для отношения вам не нужно указывать его в команде find — оно ВСЕГДА будет загружаться автоматически. При использовании QueryBuilder жадная загрузка отключена, поэтому для загрузки отношения необходимо использовать leftJoinAndSelect.

Двунаправленные отношения

Отношения бывают однонаправленными и двунаправленными. Однонаправленные отношения используют декоратор только на одной стороне. Двунаправленные отношения используют декораторы на обеих сторонах.

Мы создали однонаправленное отношение. Сделаем его двунаправленным:

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()

Отношения «многие ко многим» с пользовательскими свойствами

Если требуется добавить дополнительные свойства в отношение «многие ко многим», создайте новую сущность самостоятельно. Например, для добавления столбца order в отношение между Question и Category создайте сущность 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
}

Дополнительно потребуется добавить следующие отношения в Question и Category:

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

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