Отношения «многие ко многим»
Эта страница переведена 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[];