Transactions
Mirror manages transactions using the conn.transaction() method. Commit and rollback are automatic — you just write the business logic.
Basic Usage
Section titled “Basic Usage”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-throwThe 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.
Commit and Rollback
Section titled “Commit and Rollback”Mirror does not expose commit() or rollback() manually. The flow is always:
- Callback executed without exception →
COMMIT - Callback throws any exception →
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
Section titled “Nested Transactions — SAVEPOINT”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});What happens in each scenario
Section titled “What happens in each scenario”| Situation | SQL issued |
|---|---|
| External transaction starts | BEGIN |
| Nested transaction starts | SAVEPOINT "mirror_sp_1" |
| Nested transaction completes without error | RELEASE SAVEPOINT "mirror_sp_1" |
| Nested transaction throws exception | ROLLBACK TO SAVEPOINT "mirror_sp_1" |
| External transaction completes without error | COMMIT |
| External transaction throws exception | ROLLBACK |
Partial rollback with nested transaction
Section titled “Partial rollback with nested transaction”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});Unlimited depth
Section titled “Unlimited depth”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}); // COMMITPropagation via AsyncLocalStorage
Section titled “Propagation via AsyncLocalStorage”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 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 tudoHow it works: When entering the transaction, Mirror stores the runner in
AsyncLocalStorage. Any repository created withconn.getRepository()checks the store with each operation — if there is an active runner, it uses it automatically.
withTransaction(runner) — Explicit Link
Section titled “withTransaction(runner) — Explicit Link”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.
Pessimistic Locks
Section titled “Pessimistic Locks”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);});| Mode | SQL | Behavior |
|---|---|---|
'pessimistic_write' | FOR UPDATE | Blocks reading and writing — exclusive use |
'pessimistic_read' | FOR SHARE | Allows other readings, blocks writes |
Locks are automatically released on COMMIT or ROLLBACK.
Optimistic vs Pessimistic Competition
Section titled “Optimistic vs Pessimistic Competition”Mirror offers two approaches to concurrency control:
Optimistic (@VersionColumn) | Pessimistic (lock) | |
|---|---|---|
| Strategy | Detects conflict at UPDATE time | Locks the resource at the time of SELECT |
| Performance | Better — no line blocking | Worse in high competition |
| Failed | Releases OptimisticLockError | Transaction is on hold |
| Suitable for | Low contention, collaborative UI | High Containment, Critical Operations |
// 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);});See also:
@VersionColumn
Isolation Levels
Section titled “Isolation Levels”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});