Как работает Relation в TypeORM?
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Relation в TypeORM
Relation (отношение/связь) в TypeORM — это механизм для определения связей между сущностями (entities) в базе данных. Relations позволяют автоматически загружать связанные данные и управлять ними.
Типы relations в TypeORM
Есть четыре основных типа отношений в реляционных БД:
1. One-to-One (Один к одному)
Одна сущность связана с одной другой сущностью.
Пример: Пользователь и его профиль
// User.ts
@Entity('users')
export class User {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column()
email: string;
@OneToOne(() => UserProfile, profile => profile.user, {
cascade: true, // Удалить профиль при удалении пользователя
eager: false // Не загружай профиль автоматически
})
@JoinColumn() // Внешний ключ будет в этой таблице
profile: UserProfile;
}
// UserProfile.ts
@Entity('user_profiles')
export class UserProfile {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column()
firstName: string;
@Column()
lastName: string;
@OneToOne(() => User, user => user.profile)
user: User;
}
Использование:
const userRepo = dataSource.getRepository(User);
// Загрузить пользователя с профилем
const user = await userRepo.findOne({
where: { id: 'user-123' },
relations: ['profile'] // Явно указываем загрузить профиль
});
const firstName = user.profile.firstName; // Доступен
// Создать пользователя с профилем
const newUser = userRepo.create({
email: 'john@example.com',
profile: {
firstName: 'John',
lastName: 'Doe'
}
});
await userRepo.save(newUser);
2. One-to-Many и Many-to-One (Один ко многим)
Одна сущность связана со многими другими, а те связаны с одной.
Пример: Пользователь и его посты
// User.ts
@Entity('users')
export class User {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column()
name: string;
// Один пользователь имеет много постов
@OneToMany(() => Post, post => post.author, {
cascade: true // Удалить посты при удалении пользователя
})
posts: Post[]; // Массив!
}
// Post.ts
@Entity('posts')
export class Post {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column()
title: string;
@Column()
content: string;
// Много постов принадлежат одному пользователю
@ManyToOne(() => User, user => user.posts, {
nullable: false, // Пост всегда должен иметь автора
onDelete: 'CASCADE' // На уровне БД: удали посты если удалить пользователя
})
@JoinColumn({ name: 'author_id' }) // Внешний ключ
author: User;
@Column()
authorId: string; // Иногда нужен сам ID без загрузки всей сущности
}
Использование:
const userRepo = dataSource.getRepository(User);
// Загрузить пользователя со всеми его постами
const user = await userRepo.findOne({
where: { id: 'user-123' },
relations: ['posts']
});
for (const post of user.posts) {
console.log(post.title);
}
// Найти посты конкретного автора
const postRepo = dataSource.getRepository(Post);
const userPosts = await postRepo.find({
where: { authorId: 'user-123' },
relations: ['author']
});
// Создать пост
const newPost = postRepo.create({
title: 'My Post',
content: 'Content',
author: { id: 'user-123' } // Можно передать только ID
});
await postRepo.save(newPost);
3. Many-to-Many (Много ко многим)
Много сущностей могут быть связаны со многими другими сущностями.
Пример: Студенты и курсы
// Student.ts
@Entity('students')
export class Student {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column()
name: string;
// Студент может посещать много курсов
@ManyToMany(() => Course, course => course.students, {
cascade: true
})
@JoinTable({
name: 'student_courses', // Промежуточная таблица
joinColumn: { name: 'student_id', referencedColumnName: 'id' },
inverseJoinColumn: { name: 'course_id', referencedColumnName: 'id' }
})
courses: Course[];
}
// Course.ts
@Entity('courses')
export class Course {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column()
title: string;
// Курс могут посещать много студентов
@ManyToMany(() => Student, student => student.courses)
students: Student[];
}
Использование:
const studentRepo = dataSource.getRepository(Student);
// Загрузить студента со всеми его курсами
const student = await studentRepo.findOne({
where: { id: 'student-1' },
relations: ['courses']
});
console.log(student.courses); // Все курсы студента
// Добавить студента на курс
const student = await studentRepo.findOne({ where: { id: 'student-1' } });
const course = await courseRepo.findOne({ where: { id: 'course-1' } });
student.courses.push(course);
await studentRepo.save(student);
// Найти всех студентов на конкретном курсе
const course = await courseRepo.findOne({
where: { id: 'course-1' },
relations: ['students']
});
console.log(course.students); // Все студенты на этом курсе
4. Many-to-Many с дополнительными данными
Иногда нужно хранить дополнительные данные в промежуточной таблице.
// StudentCourse.ts — промежуточная сущность
@Entity('student_courses')
export class StudentCourse {
@PrimaryGeneratedColumn('uuid')
id: string;
@ManyToOne(() => Student)
@JoinColumn({ name: 'student_id' })
student: Student;
@ManyToOne(() => Course)
@JoinColumn({ name: 'course_id' })
course: Course;
@Column()
enrolledAt: Date = new Date();
@Column({ nullable: true })
completedAt: Date;
@Column({ default: 0 })
grade: number;
}
// Student.ts
@Entity('students')
export class Student {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column()
name: string;
@OneToMany(() => StudentCourse, sc => sc.student)
enrollments: StudentCourse[];
}
// Использование
const studentRepo = dataSource.getRepository(Student);
const student = await studentRepo.findOne({
where: { id: 'student-1' },
relations: ['enrollments', 'enrollments.course']
});
for (const enrollment of student.enrollments) {
console.log(`${enrollment.course.title} - Grade: ${enrollment.grade}`);
}
Стратегии загрузки (Loading Strategies)
1. Eager loading
@Entity('users')
export class User {
@OneToMany(() => Post, post => post.author, {
eager: true // Всегда загружай посты
})
posts: Post[];
}
const user = await userRepo.findOne({ where: { id: '1' } });
console.log(user.posts); // Уже загружены!
Плюсы: Удобно, не нужно явно указывать relations Минусы: Неэффективно, если не всегда нужны связанные данные
2. Lazy loading
@Entity('users')
export class User {
@OneToMany(() => Post, post => post.author, {
lazy: true // Загружаются по запросу
})
posts: Promise<Post[]>; // Обратите внимание на Promise!
}
const user = await userRepo.findOne({ where: { id: '1' } });
const posts = await user.posts; // Отдельный запрос
3. Явная загрузка (РЕКОМЕНДУЕТСЯ)
const user = await userRepo.findOne({
where: { id: '1' },
relations: ['posts', 'profile'] // Явно указываем, что загружать
});
Лучшие практики
1. Используй только нужные relations
// ❌ Плохо — загружаем всё подряд
const users = await userRepo.find({
relations: ['posts', 'profile', 'comments', 'followers', 'following']
});
// ✅ Хорошо — загружаем только необходимое
const users = await userRepo.find({
relations: ['profile'] // Только профиль
});
2. Избегай N+1 проблемы
// ❌ N+1 запрос (плохо!)
const users = await userRepo.find();
for (const user of users) {
const posts = await postRepo.find({ where: { authorId: user.id } }); // Запрос для каждого пользователя!
}
// ✅ Один запрос с JOIN
const users = await userRepo.find({
relations: ['posts']
});
3. Используй select для ограничения полей
const users = await userRepo.find({
select: ['id', 'name', 'email'], // Только эти поля
relations: ['profile'], // Но полный профиль
where: { isActive: true }
});
4. Cascade и onDelete для целостности
@Entity('posts')
export class Post {
@ManyToOne(() => User, user => user.posts, {
cascade: true, // Сохранить/удалить связанное при операции с родителем
onDelete: 'CASCADE', // На уровне БД: удали посты если удалить пользователя
nullable: false // Пост всегда должен иметь автора
})
author: User;
}
5. Используй QueryBuilder для сложных запросов
const users = await dataSource
.createQueryBuilder(User, 'user')
.leftJoinAndSelect('user.posts', 'posts')
.leftJoinAndSelect('user.profile', 'profile')
.where('posts.isPublished = :published', { published: true })
.orderBy('posts.createdAt', 'DESC')
.limit(10)
.getMany();
Типичные ошибки
Ошибка 1: Бесконечная рекурсия при сериализации
// ❌ Циклическая ссылка
const user = await userRepo.findOne({
where: { id: '1' },
relations: ['posts']
});
res.json(user); // User → Posts → Author → Posts ... (бесконечность!)
// ✅ Использовать классы-трансформеры
import { plainToInstance } from 'class-transformer';
class UserDTO {
id: string;
name: string;
// Без posts, чтобы избежать циклической ссылки
}
res.json(plainToInstance(UserDTO, user));
Ошибка 2: Забыли указать relations
// ❌ Undefined
const user = await userRepo.findOne({ where: { id: '1' } });
console.log(user.posts); // undefined!
// ✅ Явно указываем
const user = await userRepo.findOne({
where: { id: '1' },
relations: ['posts']
});
console.log(user.posts); // Массив постов
Итоговое сравнение типов relations
Тип Столбцы Примеры Использование
────────────────────────────────────────────────────────────────────────
One-to-One 1 в каждой User ↔ UserProfile Расширение данных
One-to-Many ∞ в many User → Posts Коллекции
Many-to-One ∞ в one Posts → User Связь с владельцем
Many-to-Many ∞ в обеих Students ↔ Courses Гибкие связи
Понимание Relations в TypeORM — это ключ к написанию эффективного backend кода с правильной структурой БД.