← Назад к вопросам

Как работает Relation в TypeORM?

2.0 Middle🔥 121 комментариев
#Базы данных и SQL#Фреймворки и библиотеки

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI29 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

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 кода с правильной структурой БД.