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

Ответ

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();

Ответ 18+ 🔞

Давай разберёмся с этими отношениями в TypeORM, а то некоторые их как мантры читают, а на деле — сплошной пиздец, если не понимать, что к чему. Это ж просто способ сказать базе: «Смотри, эта штука связана с той штукой», чтобы потом не писать эти ебушки-воробушки JOIN'ы вручную.

Основные типы, без которых нихуя не получится:

  1. @OneToOne() — это когда одна сущность привязана ровно к одной другой. Типа у юзера один профиль, и у профиля один юзер. Хуй с горы, логично же.

    @Entity()
    export class Profile {
        @PrimaryGeneratedColumn()
        id: number;
    
        // Говорим: «Эй, это связано с User'ом, а там обратная ссылка в свойстве 'profile'»
        @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) принадлежу одному юзеру.
        // И снова говорим, где обратная связь.
        @ManyToOne(() => User, user => user.photos)
        @JoinColumn({ name: 'user_id' }) // Можно даже имя колонки указать, если не нравится сгенерированное
        user: User; // Не массив, а один объект
    }

    Главное тут — не перепутать, где какая аннотация. Если начнёшь @OneToMany вешать там, где должен быть @ManyToOne, получишь пизда рулю и ошибки на ровном месте.

  3. @ManyToMany() — связь «многие ко многим». Вопросы и категории, товары и заказы — везде, где нужно связать кучу с кучей. TypeORM за тебя создаст промежуточную таблицу, красота.

    @Entity()
    export class Question {
        @PrimaryGeneratedColumn()
        id: number;
    
        // Много вопросов связано со многими категориями
        @ManyToMany(() => Category, category => category.questions)
        @JoinTable() // ВАЖНО! Эту хуйню ставишь только на ОДНОЙ из сторон связи. Обычно на той, которая «главнее».
        categories: Category[]; // И опять массив
    }

А теперь про загрузку, тут собака зарыта!

  • Eager (жадная): Данные цепляются сразу, когда грузишь основную сущность. Удобно, но может выстрелить в жопу. Представь, грузишь юзера, а у него eager: true на фотках, на постах, на комментариях — и понеслась овердохуища лишних данных и запросов.
    @ManyToOne(() => User, { eager: true }) // User подтянется всегда, даже если не просил
    user: User;
  • Lazy (ленивая): Данные подгружаются только когда ты явно на них наступишь. Возвращается Promise, так что готовь await.
    @ManyToOne(() => User, user => user.photos, { lazy: true })
    user: Promise<User>; // Смотри, тип изменился!
    // Использовать так: const user = await photo.user; // Вот тут и пойдёт запрос

    Выбор стратегии — это какого хуя вопрос удобства и оптимизации. С lazy проще накосячить с N+1, если в цикле полезешь.

Каскады (cascade) — мощная и опасная хуйня.

Включил — и TypeORM начинает сам сохранять или удалять связанные сущности.

@OneToMany(() => Photo, photo => photo.user, { cascade: true })
photos: Photo[];
// Теперь, когда делаешь userRepository.save(user), все фотки из этого массива тоже сохранятся/обновятся.
// А если удалишь юзера — фотки могут полететь в корзину. Страшно? Ещё бы.

С каскадами доверия ебать ноль, пока не проверил, что оно делает именно то, что ты хочешь.

Ну и как этим пользоваться на практике?

// Самый простой способ — указать relations в findOne
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 })
    .andWhere('photo.isPublished = :isPublished', { isPublished: true }) // И фильтруем их!
    .getOne();

QueryBuilder — это твой друг, когда стандартные методы уже не тянут. Выглядит сложнее, зато даёт контроль, а не надежду.