С какими проблемами вы сталкивались при работе с TypeORM в Node.js?

Ответ

Работая с TypeORM в Node.js, я столкнулся со следующими сложностями и нашел способы их решения:

  1. Проблема N+1 запроса: Самая частая проблема при ленивой загрузке связей ({eager: false}). TypeORM по умолчанию не предупреждает об этом.

    • Решение: Явно использовать leftJoinAndSelect в QueryBuilder или указывать relations в методе find. Для сложных сценариев писал raw SQL через query().
      // Проблема: N+1 запросов
      const users = await userRepository.find();
      for (const user of users) {
      console.log(user.photos); // Отдельный запрос для каждого пользователя!
      }
      // Решение: Загрузка связей сразу
      const usersWithPhotos = await userRepository.find({
      relations: ['photos']
      });
      // Или с QueryBuilder
      const users = await userRepository
      .createQueryBuilder('user')
      .leftJoinAndSelect('user.photos', 'photo')
      .getMany();
  2. Производительность сложных запросов: Сгенерированный SQL для QueryBuilder с множественными join и where иногда был неоптимальным.

    • Решение: Для критичных по производительности endpoint'ов я использовал нативные запросы (manager.query()) или донастраивал индексы в БД, опираясь на EXPLAIN ANALYZE.
  3. Работа с миграциями: Автогенерация миграций через typeorm migration:generate иногда пропускала изменения или генерировала лишние. Особенно это касалось переименования полей.

    • Решение: Я всегда внимательно проверял сгенерированный SQL-файл миграции перед применением. Для сложных изменений (типа изменения типа колонки с данными) писал миграции вручную, используя QueryRunner.
  4. Транзакции и управление подключением: Необходимость явно передавать EntityManager или QueryRunner в транзакцию усложняла архитектуру сервисов.

    • Решение: Я создавал абстракцию — Unit of Work паттерн или использовал вложенные транзакции через dataSource.transaction, который автоматически управляет менеджером.
  5. Типизация с кастомными репозиториями: TypeScript-типы иногда "терялись" при использовании кастомных методов репозитория, если не следовать строгому шаблону из документации.

    • Решение: Я создавал абстрактные базовые репозитории с дженериками и явно указывал типы сущностей.

Ответ 18+ 🔞

А, TypeORM, ёпта! Ну, мужик, расскажу как есть, с кровью и потом. Сидел, блядь, с этой штукой, так что доверия ебать ноль к некоторым её «фичам». Слушай историю.

Первое, что тебя накроет медным тазом — это проблема N+1 запроса. Представь: делаешь find() пользователей, вроде всё чики-пуки. А потом в цикле пытаешься к каждому фото его достать. TypeORM тебе, такая хитрая жопа, по умолчанию не скажет, что ты сейчас устроишь своей базе данных ебушки-воробушки. Для каждой строчки в результате — отдельный запрос! Овердохуища запросов, сервер просто ляжет и не встанет.

Решение простое, как валенок, но его надо знать: либо сразу говоришь, что тебе нужны связи через relations, либо берёшь в руки QueryBuilder и делаешь явный leftJoinAndSelect. Иначе будет тебе пиздопроебибна с производительностью.

// Вот так НЕ НАДО, а то будет больно
const users = await userRepository.find();
for (const user of users) {
    console.log(user.photos); // Ёпта, вот он, отдельный запрос на каждой итерации! Пиздец.
}

// А вот так — правильно, одним махом всё забираем
const usersWithPhotos = await userRepository.find({
    relations: ['photos'] // Видишь? Сказал сразу — и все фото тут как тут.
});
// Или через билдер, если запрос сложнее
const users = await userRepository
    .createQueryBuilder('user')
    .leftJoinAndSelect('user.photos', 'photo')
    .getMany();

Дальше — производительность сложных запросов. Этот QueryBuilder он, конечно, удобный, но иногда генерит такой SQL, что сам чёрт ногу сломит. С кучей вложенных JOIN и условиями — получается какая-то мартышлюшка неоптимальная.

Что делать? Для мест, где скорость — это всё, я просто переставал мудрить и писал нативный SQL через manager.query(). Или хотя бы смотрел, что там EXPLAIN ANALYZE показывает, и в БД индексы нужные настраивал. Иногда проще написать три строчки чистого SQL, чем час ебаться с билдером, пытаясь заставить его сгенерить что-то адекватное.

Третья головная боль — миграции. Автогенерация через typeorm migration:generate — это лотерея, чувак. То она изменение пропустит, то нагенерит какого-то хуя в гору, особенно если поля переименовываешь. Подозрение ебать чувствую к этому процессу.

Мой совет: никогда не применяй сгенерированную миграцию, не посмотрев её SQL! Открывай файл и смотри, что там за чудо-юдо написали. Для серьёзных вещей, вроде изменения типа колонки, где данные уже есть, лучше сразу сесть и написать миграцию вручную через QueryRunner. Сэкономишь кучу нервов.

Четвёртый пункт — транзакции. Тут, блядь, архитектура усложняется моментально. Надо таскать за собой EntityManager или QueryRunner, чтобы все действия внутри одной транзакции были. Чувствуешь себя полупидором, который передаёт эту сущность через пол-приложения.

Как выкрутился? Сделал себе абстракцию — паттерн Unit of Work. Или просто использовал dataSource.transaction() — он сам внутри создаёт менеджер и с ним работает. Гораздо чище получается, не надо мозг выносить.

Ну и последнее — типизация с кастомными репозиториями. TypeScript иногда просто терпения ноль ебать — начинает типы терять, если не танцевать с бубном строго по документации. Хочешь добавить свой метод в репозиторий — получай сюрприз.

Решение оказалось в создании абстрактных базовых репозиториев с жёстко прописанными дженериками. И везде явно указывал, какая сущность используется. Как только начал так делать — жизнь наладилась, типы перестали пропадать в неизвестном направлении.

Вот такая, блядь, эпопея. Инструмент мощный, но если не знать его подводных камней, можно легко сесть на мель. Главное — не бояться в сложных местах опускаться на уровень ниже, к чистому SQL, и всегда проверять, что там ORM нагенерила.