Ответ
Relations (отношения) в TypeORM — это декларативный способ определения связей между сущностями (таблицами) в базе данных. Они позволяют легко работать со связанными данными, как с обычными свойствами объектов в Node.js.
Основные типы отношений:
-
@OneToOne()— связь «один к одному». Часто используется для профилей или расширенных данных.@Entity() export class Profile { @PrimaryGeneratedColumn() id: number; @OneToOne(() => User, user => user.profile) // Указываем обратную связь @JoinColumn() // Столбец с внешним ключом будет в таблице `profile` user: User; } -
@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; } -
@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'ы вручную.
Основные типы, без которых нихуя не получится:
-
@OneToOne()— это когда одна сущность привязана ровно к одной другой. Типа у юзера один профиль, и у профиля один юзер. Хуй с горы, логично же.@Entity() export class Profile { @PrimaryGeneratedColumn() id: number; // Говорим: «Эй, это связано с User'ом, а там обратная ссылка в свойстве 'profile'» @OneToOne(() => User, user => user.profile) @JoinColumn() // Важный момент! Внешний ключ будет В ЭТОЙ таблице, в `profile`. Запомни, ёпта. user: User; } -
@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, получишь пизда рулю и ошибки на ровном месте. -
@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 — это твой друг, когда стандартные методы уже не тянут. Выглядит сложнее, зато даёт контроль, а не надежду.