Prisma è diventata de facto lo standard ORM per Node.js, e la sua API $transaction consente di raggruppare le operazioni in un’unica unità atomica coerente. In ambienti con un alto volume di query, soprattutto con operazioni annidate, un uso inefficace delle transazioni porta a blocchi, tempi di risposta aumentati e costi di scaling più elevati. In questo articolo esamineremo il meccanismo interno della ottimizzazione delle transazioni annidate di Prisma, mostreremo come progettare la loro struttura e quali compromessi di performance e architetturali vale la pena considerare.
Meccanismo $transaction – cosa accade realmente?
La chiamata prisma.$transaction([...]) apre una singola transazione a livello di database (es. PostgreSQL, MySQL). Prisma serializza tutte le operazioni nell’ordine fornito nell’array e le invia al motore del database come un unico blocco BEGIN … COMMIT. In caso di errore, il motore esegue automaticamente il ROLLBACK. Per le chiamate annidate Prisma utilizza i cosiddetti savepoint – punti di ripristino che consentono di annullare solo parti della transazione interna, senza interrompere l’intera operazione.
Perché è utile usare i savepoint?
I savepoint sono fondamentali quando, in una singola logica di business, dobbiamo eseguire diversi passaggi indipendenti che possono fallire, ma che non dovrebbero influire sull’intero processo. Ad esempio, nella creazione di un ordine possiamo prima riservare i prodotti e solo successivamente emettere la fattura. Se la fattura fallisce, vogliamo annullare solo quel passaggio, lasciando la riserva nel database. Prisma mappa automaticamente prisma.$transaction all’interno di un’altra transazione su SAVEPOINT, eliminando la necessità di gestirli manualmente.
Ottimizzazione delle transazioni annidate – consigli pratici
- Evita annidamenti troppo profondi. Ogni livello di savepoint genera una voce aggiuntiva nel log delle transazioni e aumenta il numero di blocchi. Si consiglia di limitarsi a 2–3 livelli.
- Usa
prisma.$executeRawsolo in casi eccezionali. Le query SQL dirette bypassano le ottimizzazioni di Prisma e possono causare incoerenze quando si usano i savepoint in parallelo. - Scegli il livello di isolamento appropriato. PostgreSQL usa di default
READ COMMITTED. In situazioni di alta concorrenza valutaREPEATABLE READoSERIALIZABLE, ma ricorda il maggior rischio di deadlock. - Monitora il numero di SAVEPOINT in una singola transazione. Superare i 10 punti è un segnale per una refactoring.
Come usare Prisma $transaction con async/await?
Prisma offre due varianti dell’API: prisma.$transaction(async (prisma) => { … }) e prisma.$transaction([op1, op2]). La seconda è più veloce, perché le operazioni vengono passate come un unico set, ma non consente di condizionare dinamicamente l’ordine. In pratica, quando è necessaria una logica condizionale, scegliamo la versione async, ricordandoci di mantenere breve la durata della transazione. Questo è un elemento chiave delle best practice dell’API delle transazioni di Prisma.
// Esempio async/await con passaggio condizionale
await prisma.$transaction(async (tx) => {
const order = await tx.order.create({ data: { userId, status: 'PENDING' } });
if (needsInvoice) {
await tx.invoice.create({ data: { orderId: order.id, amount } });
}
// Savepoint creato automaticamente per la chiamata interna
});
Common pitfalls Prisma transactions in production
Uno degli errori più frequenti è fare affidamento sul livello di isolamento predefinito in presenza di scritture intensive. Un’elevata concorrenza può causare lost updates, che in PostgreSQL si manifestano come serialization_failure. La soluzione è aumentare il livello di isolamento oppure introdurre lock ottimisti (versionamento dei record). Un’altra trappola è lasciare transazioni aperte nel codice asincrono – le performance delle transazioni annidate di Prisma diminuiscono drasticamente quando le connessioni non vengono rilasciate.
Esempi avanzati: elaborazione batch e logica di retry
Nei casi batch, dove elaboriamo centinaia di record, è utile combinare prisma.$transaction con un meccanismo di retry. Il codice seguente dimostra come gestire facilmente i tentativi ripetuti mantenendo la consistenza:
async function processBatch(items) {
const MAX_RETRIES = 3;
for (const item of items) {
let attempt = 0;
while (attempt < MAX_RETRIES) {
try {
await prisma.$transaction(async (tx) => {
await tx.inventory.update({
where: { id: item.inventoryId },
data: { quantity: { decrement: item.qty } },
});
await tx.orderItem.create({ data: item });
});
break; // successo, passa all'elemento successivo
} catch (e) {
if (e.code === 'P0001' || e.code === '40001') {
attempt++;
await new Promise(r => setTimeout(r, 100 * attempt));
} else {
throw e; // errore non gestito
}
}
}
}
}
Strategie di isolamento delle transazioni in Prisma
Per impostazione predefinita Prisma utilizza il livello READ COMMITTED. In base ai requisiti di business, possiamo scegliere:
- READ COMMITTED – veloce, ma soggetto a letture non ripetibili.
- REPEATABLE READ – garantisce una vista stabile dei dati durante la transazione, utile per i report.
- SERIALIZABLE – livello di isolamento più alto, elimina le phantom reads, ma aumenta il rischio di deadlock e riduce le prestazioni delle transazioni annidate di Prisma.
Il livello di isolamento si definisce in schema.prisma nella sezione datasource, ad esempio isolation_level = "Serializable". Ricorda di testare l'impatto sulle prestazioni in ambiente di staging.
Quando usare e quando evitare le transazioni annidate?
- Usa quando le operazioni sono strettamente correlate e devono mantenere la coerenza (ad es. ordine + pagamento + aggiornamento delle scorte).
- Evita quando è possibile suddividere la logica in servizi separati e idempotenti che comunicano tramite code (event‑driven). Questo riduce il numero di lock e consente lo scaling orizzontale.
Trappole comuni in produzione
Oltre ai problemi di isolamento già citati, nella pratica compaiono anche:
- Utilizzo di transazioni di lunga durata nelle richieste HTTP – aumenta il rischio di timeout.
- Mancanza di timeout (
timeout) suprisma.$transaction, che può bloccare le connessioni sotto carico elevato. - Gestione inadeguata delle connessioni nel pool – con un alto numero di transazioni concorrenti il pool può esaurirsi.
Checklist – ottimizzazione delle transazioni annidate
- Verifica che tutte le operazioni nella transazione siano davvero necessarie – estrai le parti idempotenti.
- Limita l'annidamento a un massimo di due livelli.
- Imposta il livello di isolamento appropriato nella configurazione del
datasource(ad es.isolation_level = "Serializable"inschema.prisma). - Monitora il numero di
SAVEPOINTnei log del database – più di 10 in una singola transazione è un segnale per refactoring. - Testa scenari di deadlock in ambiente di staging usando
pgbencho strumenti simili. - Aggiungi un timeout a
prisma.$transaction(ad es.{ timeout: 5000 }) per evitare blocchi.
Riepilogo e CTA
L'ottimizzazione delle transazioni annidate di Prisma richiede la comprensione dei savepoint, la scelta consapevole del livello di isolamento e la limitazione della profondità di annidamento. Applicando le best practice dell'API di transazione di Prisma descritte, ridurrai i lock e aumenterai la capacità di throughput dell'applicazione in ambienti di produzione. Se hai bisogno di assistenza per il refactoring del codice, audit delle prestazioni o progettazione di un'architettura scalabile basata su Prisma, contatta il team di Coderia.it – insieme porteremo il tuo database al livello successivo.



