Transações no TypeORM: Atomicidade e o Padrão Unit of Work
Como garantir integridade de dados usando QueryRunner. O problema do vazamento de abstração ao injetar o EntityManager nos Use Cases e como o padrão Unit of Work (UoW) resolve o isolamento arquitetural.
Se um Use Case desconta o saldo do cliente (UPDATE) e falha logo antes de criar o registro do pedido (INSERT), você acabou de roubar o dinheiro do usuário sem entregar o produto. Operações que envolvem múltiplas tabelas e dependem umas das outras exigem Transações: ou tudo funciona (Commit), ou tudo é desfeito (Rollback).
A Abordagem Básica (e Acoplada) com QueryRunner
A documentação do TypeORM ensina a usar o QueryRunner para controlar a transação manualmente. O problema dessa abordagem é que ela vaza conceitos do TypeORM direto para a camada de regra de negócio (Use Case), quebrando a Arquitetura Limpa.
import { dataSource } from '../infra/typeorm';
export class CheckoutUseCase {
async execute(orderId: string, userId: string) {
const queryRunner = dataSource.createQueryRunner();
await queryRunner.connect();
await queryRunner.startTransaction();
try {
// PROBLEMA 1: O Use Case conhece entidades do TypeORM (OrderModel)
const orderRepo = queryRunner.manager.getRepository(OrderModel);
const userRepo = queryRunner.manager.getRepository(UserModel);
const user = await userRepo.findOneBy({ id: userId });
user.balance -= 100;
await userRepo.save(user);
const order = orderRepo.create({ status: 'PAID' });
await orderRepo.save(order);
await queryRunner.commitTransaction();
} catch (err) {
await queryRunner.rollbackTransaction();
throw err;
} finally {
await queryRunner.release();
}
}
}A Solução Elegante: Unit of Work (UoW)
O problema do QueryRunner exposto no Use Case não é apenas estético: ele torna o Use Case intestavível de forma unitária e acopla a lógica de negócio ao TypeORM. Se você quiser trocar o ORM por outro (ou escrever um FakeUnitOfWork para testes), precisa reescrever o Use Case inteiro. Para não acoplar o Use Case ao TypeORM e manter o Repository Pattern, usamos o padrão Unit of Work.
Para não acoplar o Use Case ao TypeORM e manter o Repository Pattern, usamos o padrão Unit of Work. Ele abstrai o conceito de transação em uma interface genérica: o Use Case chama uow.transaction(callback) e recebe repositórios isolados dentro daquele contexto.
import type { IUsersRepository } from '../repositories/IUsersRepository';
import type { IOrdersRepository } from '../repositories/IOrdersRepository';
export interface IWorkContext {
usersRepo: IUsersRepository;
ordersRepo: IOrdersRepository;
}
export interface IUnitOfWork {
// Recebe um callback que executa dentro da transação
transaction<T>(work: (ctx: IWorkContext) => Promise<T>): Promise<T>;
}import { injectable, inject } from 'tsyringe';
import type { IUnitOfWork } from '../domain/providers/IUnitOfWork';
@injectable()
export class CheckoutUseCase {
constructor(
@inject('UnitOfWork')
private uow: IUnitOfWork
) {}
async execute(orderId: string, userId: string) {
// A transação é iniciada aqui. Se o callback lançar erro, faz rollback automático.
await this.uow.transaction(async ({ usersRepo, ordersRepo }) => {
// Usamos os repositórios injetados no contexto da transação
const user = await usersRepo.findById(userId);
if (!user || user.balance < 100) throw new Error('Saldo insuficiente');
user.balance -= 100;
await usersRepo.save(user); // Faz parte da transação
const order = await ordersRepo.findById(orderId);
order.status = 'PAID';
await ordersRepo.save(order); // Faz parte da transação
// Se der sucesso, o UoW faz o commitTransaction por trás dos panos
});
}
}Implementando o Unit of Work no TypeORM
O TypeORMUnitOfWork encapsula todo o QueryRunner por baixo dos panos. O ponto crítico é instanciar os repositórios passando o queryRunner.manager — sem isso, o repositório usaria um EntityManager diferente, fora da transação, e as operações não seriam atômicas. O Try/Catch/Finally garante que o queryRunner sempre seja liberado de volta ao pool de conexões, mesmo se o rollback falhar.
import { DataSource } from 'typeorm';
import { inject, injectable } from 'tsyringe';
import type { IUnitOfWork, IWorkContext } from '../../../domain/providers/IUnitOfWork';
import { TypeORMUsersRepository } from '../repositories/TypeORMUsersRepository';
import { TypeORMOrdersRepository } from '../repositories/TypeORMOrdersRepository';
@injectable()
export class TypeORMUnitOfWork implements IUnitOfWork {
constructor(
@inject('DataSource')
private dataSource: DataSource
) {}
async transaction<T>(work: (ctx: IWorkContext) => Promise<T>): Promise<T> {
const queryRunner = this.dataSource.createQueryRunner();
await queryRunner.connect();
await queryRunner.startTransaction();
try {
// Instancia os repositórios passando o entityManager da transação
// IMPORTANTE: o repositório precisa estar preparado para receber o manager opcional
const ctx: IWorkContext = {
usersRepo: new TypeORMUsersRepository(queryRunner.manager),
ordersRepo: new TypeORMOrdersRepository(queryRunner.manager),
};
// Executa a regra de negócio
const result = await work(ctx);
await queryRunner.commitTransaction();
return result;
} catch (err) {
await queryRunner.rollbackTransaction();
throw err;
} finally {
await queryRunner.release();
}
}
}Cuidado com deadlocks: Transações bloqueiam as linhas editadas (e às vezes tabelas inteiras) até que o commit ou rollback aconteça. NUNCA faça requisições externas demoradas (ex: chamar a API do Stripe) DENTRO do bloco da transação. Faça a chamada HTTP antes, e abra a transação apenas no momento exato de salvar os dados no banco.
Conclusão
Transações são obrigatórias para escritas atômicas múltiplas, mas poluem a camada de domínio se feitas diretamente com o QueryRunner do TypeORM. O Unit of Work atua como uma fachada: isola o banco de dados e fornece um escopo transacional limpo para os Use Cases, mantendo a arquitetura testável e permitindo a criação de um FakeUnitOfWork (em memória) para os testes unitários.