Transactions
O Mirror gerencia transações pelo método conn.transaction(). O commit e rollback são automáticos — você escreve apenas a lógica de negócio.
Uso Básico
Seção intitulada “Uso Básico”const result = await conn.transaction(async (trx) => { const users = trx.getRepository(User); const orders = trx.getRepository(Order);
const user = await users.findOneOrFail({ where: { id: 1 } });
const order = new Order(); order.userId = user.id; order.total = 99.90;
await orders.save(order); return order;});// Se nenhuma exceção for lançada → COMMIT automático// Se uma exceção for lançada → ROLLBACK automático + re-throwO callback recebe um TransactionContext com um getRepository() vinculado à transação. Use sempre esse repositório dentro do callback — nunca o repositório obtido fora da transação.
Commit e Rollback
Seção intitulada “Commit e Rollback”O Mirror não expõe commit() ou rollback() manualmente. O fluxo é sempre:
- Callback executado sem exceção →
COMMIT - Callback lança qualquer exceção →
ROLLBACK+ re-throw
try { await conn.transaction(async (trx) => { const repo = trx.getRepository(Account);
const from = await repo.findOneOrFail({ where: { id: fromId } }); const to = await repo.findOneOrFail({ where: { id: toId } });
if (from.balance < amount) { throw new Error('Saldo insuficiente'); // → ROLLBACK automático }
from.balance -= amount; to.balance += amount;
await repo.save(from); await repo.save(to); }); // → COMMIT} catch (err) { // Transação já foi revertida aqui console.error(err.message);}Nested Transactions — SAVEPOINT
Seção intitulada “Nested Transactions — SAVEPOINT”Chamar conn.transaction() dentro de outra transação não abre uma segunda transação. O Mirror detecta automaticamente o contexto ativo e usa um SAVEPOINT nomeado no lugar.
await conn.transaction(async (trx) => { const posts = trx.getRepository(Post);
await posts.save(mainPost);
// Chamada aninhada — Mirror emite SAVEPOINT "mirror_sp_1" await conn.transaction(async (inner) => { const tags = inner.getRepository(Tag); await tags.save(newTag); // Sucesso → RELEASE SAVEPOINT "mirror_sp_1" });
// Continua na transação externa normalmente await posts.save(anotherPost); // Fim do callback externo → COMMIT});O que acontece em cada cenário
Seção intitulada “O que acontece em cada cenário”| Situação | SQL emitido |
|---|---|
| Transação externa inicia | BEGIN |
| Nested transaction inicia | SAVEPOINT "mirror_sp_1" |
| Nested transaction conclui sem erro | RELEASE SAVEPOINT "mirror_sp_1" |
| Nested transaction lança exceção | ROLLBACK TO SAVEPOINT "mirror_sp_1" |
| Transação externa conclui sem erro | COMMIT |
| Transação externa lança exceção | ROLLBACK |
Rollback parcial com nested transaction
Seção intitulada “Rollback parcial com nested transaction”O poder do SAVEPOINT está em permitir reverter apenas uma parte do trabalho sem desfazer tudo:
await conn.transaction(async (trx) => { const logs = trx.getRepository(AuditLog); const jobs = trx.getRepository(Job);
await logs.save(auditEntry); // salvo na transação principal
try { await conn.transaction(async (inner) => { const jobs = inner.getRepository(Job); await jobs.save(riskyJob); // Simula falha throw new Error('job inválido'); // → ROLLBACK TO SAVEPOINT — apenas riskyJob é desfeito }); } catch { // Engole o erro — auditEntry ainda está na transação }
// COMMIT — apenas auditEntry é persistido});Profundidade ilimitada
Seção intitulada “Profundidade ilimitada”SAVEPOINTs são nomeados com um contador incremental (mirror_sp_1, mirror_sp_2, …), então você pode aninhar transações em qualquer profundidade:
await conn.transaction(async (trx) => { // BEGIN await conn.transaction(async (inner1) => { // SAVEPOINT mirror_sp_1 await conn.transaction(async (inner2) => { // SAVEPOINT mirror_sp_2 // ... }); // RELEASE mirror_sp_2 }); // RELEASE mirror_sp_1}); // COMMITPropagação via AsyncLocalStorage
Seção intitulada “Propagação via AsyncLocalStorage”O Mirror usa AsyncLocalStorage do Node.js para propagar o runner da transação de forma transparente pelo contexto assíncrono. Isso significa que você pode chamar serviços que fazem queries sem precisar passar o contexto manualmente.
class OrderService { private repo = conn.getRepository(Order);
async create(data: Partial<Order>) { // Este método não sabe se está dentro de uma transação return this.repo.save(Object.assign(new Order(), data)); }}
class PaymentService { private repo = conn.getRepository(Payment);
async charge(orderId: number, amount: number) { return this.repo.save(Object.assign(new Payment(), { orderId, amount })); }}
// No controller / use caseawait conn.transaction(async (trx) => { // Os repositórios de orderService e paymentService // automaticamente "enxergam" esta transação via AsyncLocalStorage const order = await orderService.create({ userId: 1 }); const payment = await paymentService.charge(order.id, 99.90);});// Se qualquer operação falhar → ROLLBACK de tudoComo funciona: Ao entrar na transação, o Mirror armazena o runner no
AsyncLocalStorage. Qualquer repositório criado comconn.getRepository()verifica o store a cada operação — se houver um runner ativo, ele o usa automaticamente.
withTransaction(runner) — Vínculo Explícito
Seção intitulada “withTransaction(runner) — Vínculo Explícito”Se você precisar vincular um repositório a uma transação específica de forma explícita (sem depender do AsyncLocalStorage), use repo.withTransaction():
await conn.transaction(async (trx) => { const runner = trx.runner;
// Repositório externo forçado a usar este runner const externalRepo = someExternalService.getRepo().withTransaction(runner); await externalRepo.save(entity);});Um repositório vinculado com withTransaction ignora o AsyncLocalStorage e usa apenas o runner fixado. Útil quando a propagação automática não é desejada ou quando você integra com código legado.
Locks Pessimistas
Seção intitulada “Locks Pessimistas”Use lock dentro de uma transação para bloquear linhas explicitamente. Sem uma transação ativa, o lock não tem efeito prático.
await conn.transaction(async (trx) => { const repo = trx.getRepository(Product);
// Bloqueia a linha para escrita — outras transações ficam em espera const product = await repo.findOne({ where: { id: productId }, lock: 'pessimistic_write', // FOR UPDATE });
if (!product || product.stock < quantity) { throw new Error('Estoque insuficiente'); }
product.stock -= quantity; await repo.save(product);});| Modo | SQL | Comportamento |
|---|---|---|
'pessimistic_write' | FOR UPDATE | Bloqueia leitura e escrita — uso exclusivo |
'pessimistic_read' | FOR SHARE | Permite outras leituras, bloqueia escritas |
Os locks são liberados automaticamente no COMMIT ou ROLLBACK.
Concorrência Otimista vs Pessimista
Seção intitulada “Concorrência Otimista vs Pessimista”O Mirror oferece duas abordagens para controle de concorrência:
Optimistic (@VersionColumn) | Pessimistic (lock) | |
|---|---|---|
| Estratégia | Detecta conflito na hora do UPDATE | Bloqueia o recurso na hora do SELECT |
| Performance | Melhor — sem bloqueio de linha | Pior em alta concorrência |
| Falha | Lança OptimisticLockError | Transação fica em espera |
| Indicado para | Baixa contenção, UI colaborativa | Alta contenção, operações críticas |
// Optimistic — tenta salvar, falha se outro atualizou antesimport { OptimisticLockError } from 'mirror-orm';
try { await repo.save(product); // tem @VersionColumn} catch (err) { if (err instanceof OptimisticLockError) { // Re-ler e tentar novamente }}
// Pessimistic — bloqueia primeiro, nunca falha por conflitoawait conn.transaction(async (trx) => { const product = await trx.getRepository(Product).findOne({ where: { id }, lock: 'pessimistic_write', }); // garantido que ninguém mais está modificando aqui product.stock -= 1; await trx.getRepository(Product).save(product);});Ver também:
@VersionColumn
Isolation Levels
Seção intitulada “Isolation Levels”O Mirror usa o nível de isolamento padrão do banco de dados. Configuração explícita de isolation level não é suportada — use comandos SQL diretos se precisar:
await conn.transaction(async (trx) => { await trx.query('SET TRANSACTION ISOLATION LEVEL SERIALIZABLE');
// ... operações da transação});