Pular para o conteúdo

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.


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-throw

O 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.


O Mirror não expõe commit() ou rollback() manualmente. O fluxo é sempre:

  • Callback executado sem exceçãoCOMMIT
  • Callback lança qualquer exceçãoROLLBACK + 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);
}

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
});
SituaçãoSQL emitido
Transação externa iniciaBEGIN
Nested transaction iniciaSAVEPOINT "mirror_sp_1"
Nested transaction conclui sem erroRELEASE SAVEPOINT "mirror_sp_1"
Nested transaction lança exceçãoROLLBACK TO SAVEPOINT "mirror_sp_1"
Transação externa conclui sem erroCOMMIT
Transação externa lança exceçãoROLLBACK

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
});

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
}); // COMMIT

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 case
await 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 tudo

Como funciona: Ao entrar na transação, o Mirror armazena o runner no AsyncLocalStorage. Qualquer repositório criado com conn.getRepository() verifica o store a cada operação — se houver um runner ativo, ele o usa automaticamente.


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.


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);
});
ModoSQLComportamento
'pessimistic_write'FOR UPDATEBloqueia leitura e escrita — uso exclusivo
'pessimistic_read'FOR SHAREPermite outras leituras, bloqueia escritas

Os locks são liberados automaticamente no COMMIT ou ROLLBACK.


O Mirror oferece duas abordagens para controle de concorrência:

Optimistic (@VersionColumn)Pessimistic (lock)
EstratégiaDetecta conflito na hora do UPDATEBloqueia o recurso na hora do SELECT
PerformanceMelhor — sem bloqueio de linhaPior em alta concorrência
FalhaLança OptimisticLockErrorTransação fica em espera
Indicado paraBaixa contenção, UI colaborativaAlta contenção, operações críticas
// Optimistic — tenta salvar, falha se outro atualizou antes
import { 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 conflito
await 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


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
});