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

«Как работает Relation в TypeORM» — вопрос из категории ORM, который задают на 26% собеседований Node.js Разработчик. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

Relations (отношения) в TypeORM — это декларативный способ определения связей между сущностями (таблицами) в базе данных. Они позволяют легко работать со связанными данными, как с обычными свойствами объектов в Node.js.

Основные типы отношений:

  1. @OneToOne() — связь «один к одному». Часто используется для профилей или расширенных данных.

    @Entity()
    export class Profile {
        @PrimaryGeneratedColumn()
        id: number;
    
        @OneToOne(() => User, user => user.profile) // Указываем обратную связь
        @JoinColumn() // Столбец с внешним ключом будет в таблице `profile`
        user: User;
    }
  2. @OneToMany() и @ManyToOne() — связь «один ко многим». Самая распространенная.

    @Entity()
    export class User {
        @PrimaryGeneratedColumn()
        id: number;
    
        // У одного User много Photo
        @OneToMany(() => Photo, photo => photo.user)
        photos: Photo[];
    }
    
    @Entity()
    export class Photo {
        @PrimaryGeneratedColumn()
        id: number;
    
        // Много Photo принадлежат одному User
        @ManyToOne(() => User, user => user.photos)
        @JoinColumn({ name: 'user_id' })
        user: User;
    }
  3. @ManyToMany() — связь «многие ко многим». Создает промежуточную таблицу автоматически.

    @Entity()
    export class Question {
        @PrimaryGeneratedColumn()
        id: number;
    
        @ManyToMany(() => Category, category => category.questions)
        @JoinTable() // Эта аннотация ставится только на одной из сторон
        categories: Category[];
    }

Стратегии загрузки (Loading Strategies):

  • Eager: Данные загружаются автоматически вместе с основной сущностью. Может привести к N+1 проблеме, если неаккуратно использовать.
    @ManyToOne(() => User, { eager: true }) // User будет подгружен всегда
    user: User;
  • Lazy: Данные загружаются только при явном обращении (возвращается Promise).
    @ManyToOne(() => User, user => user.photos, { lazy: true })
    user: Promise<User>; // Тип - Promise
    // Использование: const user = await photo.user;

Каскадные операции (cascade): Позволяют автоматически сохранять/удалять связанные сущности.

@OneToMany(() => Photo, photo => photo.user, { cascade: true })
photos: Photo[];
// При сохранении userRepository.save(user) сохранятся и все его photos

Практический пример с запросом:

// Найти пользователя со всеми его фотографиями
const userWithPhotos = await userRepository.findOne({
    where: { id: 1 },
    relations: ['photos'] // Жадная загрузка для этого запроса
});

// Или с помощью QueryBuilder для большей гибкости
const user = await userRepository
    .createQueryBuilder('user')
    .leftJoinAndSelect('user.photos', 'photo')
    .where('user.id = :id', { id: 1 })
    .getOne();