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

Часто задаваемые вопросы по связям

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

Эта страница переведена 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
}

Как загружать связи в сущностях?

Самый простой способ — опция relations в FindOptions:

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 JOIN и INNER 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
}