NestJSSeniorCoding

Как обрабатывать транзакции базы данных в NestJS с TypeORM?

В NestJS+TypeORM транзакции оформляют через DataSource.transaction(), QueryRunner с явным управлением или декоратор @Transaction(). Предпочтителен QueryRunner: он даёт полный контроль над коммитом, откатом и вложенными savepoint-ами.

Транзакции в NestJS с TypeORM

TypeORM предоставляет несколько механизмов для работы с транзакциями. Выбор подхода зависит от требований к контролю над транзакцией, возможности вложенных операций и тестируемости.

Подход 1: DataSource.transaction() — простой callback

Самый компактный способ для атомарных операций без сложной логики:

import { Injectable } from '@nestjs/common';
import { DataSource } from 'typeorm';
import { User } from './user.entity';
import { Wallet } from './wallet.entity';

@Injectable()
export class UserService {
  constructor(private readonly dataSource: DataSource) {}

  async createUserWithWallet(name: string, email: string): Promise<User> {
    return this.dataSource.transaction(async (manager) => {
      const user = manager.create(User, { name, email });
      await manager.save(user);

      const wallet = manager.create(Wallet, { userId: user.id, balance: 0 });
      await manager.save(wallet);

      return user; // при выбросе исключения — автоматический rollback
    });
  }
}

Подход 2: QueryRunner — полный контроль

Рекомендуется для сложных сценариев: savepoint, условный rollback, несколько этапов коммита:

import { Injectable, InternalServerErrorException } from '@nestjs/common';
import { DataSource, QueryRunner } from 'typeorm';
import { Order } from './order.entity';
import { Product } from './product.entity';

@Injectable()
export class OrderService {
  constructor(private readonly dataSource: DataSource) {}

  async placeOrder(userId: string, productId: string, qty: number): Promise<Order> {
    const qr: QueryRunner = this.dataSource.createQueryRunner();
    await qr.connect();
    await qr.startTransaction('SERIALIZABLE'); // уровень изоляции явно

    try {
      const product = await qr.manager.findOneOrFail(Product, {
        where: { id: productId },
        lock: { mode: 'pessimistic_write' }, // SELECT ... FOR UPDATE
      });

      if (product.stock < qty) {
        throw new Error('Недостаточно товара на складе');
      }

      product.stock -= qty;
      await qr.manager.save(product);

      const order = qr.manager.create(Order, { userId, productId, qty });
      await qr.manager.save(order);

      await qr.commitTransaction();
      return order;
    } catch (err) {
      await qr.rollbackTransaction();
      throw new InternalServerErrorException(err.message);
    } finally {
      await qr.release(); // всегда освобождаем соединение
    }
  }
}

Подход 3: SAVEPOINT для вложенных транзакций

async complexFlow(qr: QueryRunner): Promise<void> {
  await qr.startTransaction();
  try {
    await qr.manager.save(MainEntity, { /* ... */ });

    // Создаём savepoint перед рискованной операцией
    await qr.query('SAVEPOINT sp1');
    try {
      await qr.manager.save(RiskyEntity, { /* ... */ });
    } catch {
      await qr.query('ROLLBACK TO SAVEPOINT sp1'); // частичный откат
    }

    await qr.commitTransaction();
  } catch (err) {
    await qr.rollbackTransaction();
    throw err;
  } finally {
    await qr.release();
  }
}

Инкапсуляция через декоратор (DRY-подход)

// transaction.decorator.ts
import { DataSource } from 'typeorm';

export function Transactional() {
  return function (
    _target: object,
    _key: string,
    descriptor: PropertyDescriptor,
  ) {
    const original = descriptor.value;
    descriptor.value = async function (...args: unknown[]) {
      const dataSource: DataSource = this.dataSource; // инжектируется в класс
      return dataSource.transaction((manager) =>
        original.apply({ ...this, manager }, args),
      );
    };
    return descriptor;
  };
}

Уровни изоляции в TypeORM

await qr.startTransaction('READ COMMITTED');  // по умолчанию в PostgreSQL
await qr.startTransaction('REPEATABLE READ');
await qr.startTransaction('SERIALIZABLE');    // максимальная защита, больше блокировок

Подводные камни

  • Утечка соединений при пропуске qr.release() — если не вызвать release() в блоке finally, соединение не вернётся в пул и приложение рано или поздно «зависнет» из-за исчерпания пула.
  • Использование обычного Repository внутри QueryRunner — инжектированный через @InjectRepository() репозиторий не участвует в транзакции QueryRunner. Для операций в транзакции используйте qr.manager.getRepository(Entity).
  • Дедлоки при SERIALIZABLE — при высокой конкурентности уровень SERIALIZABLE порождает сериализационные ошибки (ERROR: 40001). Нужна логика повторных попыток (retry) с экспоненциальной задержкой.
  • Async context propagation — при передаче QueryRunner между сервисами через аргументы нарушается инкапсуляция. Рассмотрите использование AsyncLocalStorage или библиотеки typeorm-transactional для прозрачной передачи контекста.
  • N+1 внутри транзакции — ленивые отношения TypeORM открывают новые соединения, игнорируя активный QueryRunner. Всегда загружайте связи явно через relations или qr.manager.find().
  • Тестирование транзакций — мокировать DataSource сложно. Удобнее поднимать реальную тестовую БД (через testcontainers) и откатывать каждый тест через qr.rollbackTransaction().
  • Уровень изоляции по умолчанию — TypeORM не выставляет уровень изоляции явно; используется дефолт СУБД. В PostgreSQL это READ COMMITTED, а в MySQL — REPEATABLE READ. При смешанном окружении это неожиданно.

Common mistakes

  • Дает общий ответ про Node.js и не называет конкретные API NestJS.
  • Не объясняет, где в lifecycle находится транзакции TypeORM в NestJS.
  • Не разделяет validation, authorization, business logic и persistence.
  • Игнорирует ошибки, лимиты входных данных, observability и тестирование.

What the interviewer is testing

  • Может объяснить транзакции TypeORM в NestJS на примере кода.
  • Называет ключевые API: DataSource.transaction().
  • Использует точные API NestJS, а не вымышленные hooks/decorators/methods.
  • Видит production-риски: безопасность, отказоустойчивость, логирование и тесты.

Sources

Related topics