Skip to content

Transactions

Mirror manages transactions using the conn.transaction() method. Commit and rollback are automatic — you just write the business logic.


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

The callback receives a TransactionContext with a getRepository() linked to the transaction. Always use this repository inside the callback — never the repository obtained outside the transaction.


Mirror does not expose commit() or rollback() manually. The flow is always:

  • Callback executed without exceptionCOMMIT
  • Callback throws any exceptionROLLBACK + 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);
}

Calling conn.transaction() within another transaction does not open a second transaction. Mirror automatically detects the active context and uses a named SAVEPOINT instead.

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
});
SituationSQL issued
External transaction startsBEGIN
Nested transaction startsSAVEPOINT "mirror_sp_1"
Nested transaction completes without errorRELEASE SAVEPOINT "mirror_sp_1"
Nested transaction throws exceptionROLLBACK TO SAVEPOINT "mirror_sp_1"
External transaction completes without errorCOMMIT
External transaction throws exceptionROLLBACK

The power of SAVEPOINT is that it allows you to revert just a part of the work without undoing everything:

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 are named with an incremental counter (mirror_sp_1, mirror_sp_2, …), so you can nest transactions to any depth:

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

Mirror uses Node.js’ AsyncLocalStorage to propagate the transaction runner transparently across the asynchronous context. This means that you can call services that make queries without having to pass the context manually.

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

How it works: When entering the transaction, Mirror stores the runner in AsyncLocalStorage. Any repository created with conn.getRepository() checks the store with each operation — if there is an active runner, it uses it automatically.


If you need to link a repository to a specific transaction explicitly (without relying on 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);
});

A repository linked with withTransaction ignores AsyncLocalStorage and uses only the pinned runner. Useful when automatic propagation is not desired or when you integrate with legacy code.


Use lock within a transaction to explicitly block rows. Without an active transaction, the lock has no practical effect.

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);
});
ModeSQLBehavior
'pessimistic_write'FOR UPDATEBlocks reading and writing — exclusive use
'pessimistic_read'FOR SHAREAllows other readings, blocks writes

Locks are automatically released on COMMIT or ROLLBACK.


Mirror offers two approaches to concurrency control:

Optimistic (@VersionColumn)Pessimistic (lock)
StrategyDetects conflict at UPDATE timeLocks the resource at the time of SELECT
PerformanceBetter — no line blockingWorse in high competition
FailedReleases OptimisticLockErrorTransaction is on hold
Suitable forLow contention, collaborative UIHigh Containment, Critical Operations
// 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);
});

See also: @VersionColumn


Mirror uses the database’s default isolation level. Explicit isolation level setting is not supported — use direct SQL commands if you need to:

await conn.transaction(async (trx) => {
await trx.query('SET TRANSACTION ISOLATION LEVEL SERIALIZABLE');
// ... operações da transação
});