Transazioni
Transazioni in Prisma
Le transazioni garantiscono che un gruppo di operazioni venga eseguito in modo atomico: o tutte riescono, o nessuna viene applicata. Prisma offre diversi modi per gestire le transazioni.
Nested Writes (Transazioni Implicite)
Le operazioni di creazione e aggiornamento annidati sono automaticamente transazionali:
// Questa operazione e' gia' una transazione implicita
const user = await prisma.user.create({
data: {
email: 'mario@example.com',
name: 'Mario',
profile: {
create: { bio: 'Sviluppatore' },
},
posts: {
create: [
{ title: 'Post 1' },
{ title: 'Post 2' },
],
},
},
})
// Se la creazione di un post fallisce, l'utente e il profilo
// NON vengono creati (rollback automatico).
$transaction Sequenziale
Passa un array di operazioni Prisma. Vengono eseguite in ordine e in una singola transazione:
const [deletedPosts, deletedUser] = await prisma.$transaction([
prisma.post.deleteMany({ where: { authorId: 1 } }),
prisma.user.delete({ where: { id: 1 } }),
])
console.log(`Eliminati ${deletedPosts.count} post e l'utente`)
Esempio pratico: trasferimento di crediti tra utenti:
const amount = 100
const [sender, receiver] = await prisma.$transaction([
prisma.user.update({
where: { id: 1 },
data: { balance: { decrement: amount } },
}),
prisma.user.update({
where: { id: 2 },
data: { balance: { increment: amount } },
}),
])
$transaction Interattiva
Per logica condizionale all’interno della transazione, usa la versione interattiva con callback:
const transfer = await prisma.$transaction(async (tx) => {
// 1. Verifica il saldo del mittente
const sender = await tx.user.findUniqueOrThrow({
where: { id: 1 },
})
if (sender.balance < 100) {
throw new Error('Saldo insufficiente')
}
// 2. Decrementa il saldo del mittente
const updatedSender = await tx.user.update({
where: { id: 1 },
data: { balance: { decrement: 100 } },
})
// 3. Incrementa il saldo del destinatario
const updatedReceiver = await tx.user.update({
where: { id: 2 },
data: { balance: { increment: 100 } },
})
// 4. Registra la transazione
const log = await tx.transactionLog.create({
data: {
senderId: 1,
receiverId: 2,
amount: 100,
type: 'TRANSFER',
},
})
return { sender: updatedSender, receiver: updatedReceiver, log }
})
// Se una qualsiasi operazione fallisce o viene lanciato un errore,
// TUTTE le operazioni vengono annullate automaticamente.
Isolation Level
Puoi specificare il livello di isolamento della transazione:
const result = await prisma.$transaction(
async (tx) => {
// Le letture vedranno uno snapshot consistente
const users = await tx.user.findMany()
const count = await tx.user.count()
return { users, count }
},
{
isolationLevel: 'Serializable', // Massimo isolamento
maxWait: 5000, // Tempo massimo di attesa per la transazione (ms)
timeout: 10000, // Timeout di esecuzione (ms)
}
)
Livelli disponibili (dipendono dal database):
| Livello | Descrizione |
|---|---|
ReadUncommitted |
Legge dati non ancora committati |
ReadCommitted |
Legge solo dati committati (default PostgreSQL) |
RepeatableRead |
Garantisce letture ripetibili (default MySQL) |
Serializable |
Isolamento massimo, esecuzione sequenziale |
Snapshot |
Snapshot isolation (SQL Server) |
Gestione Errori
Errori nelle Transazioni Sequenziali
try {
const result = await prisma.$transaction([
prisma.user.create({ data: { email: 'a@b.com', name: 'A' } }),
prisma.user.create({ data: { email: 'a@b.com', name: 'B' } }), // Errore: email duplicata
])
} catch (error) {
if (error.code === 'P2002') {
console.error('Vincolo di unicita\' violato, transazione annullata')
}
}
Errori nelle Transazioni Interattive
import { Prisma } from '@prisma/client'
async function processOrder(orderId: number) {
try {
const result = await prisma.$transaction(async (tx) => {
const order = await tx.order.findUniqueOrThrow({
where: { id: orderId },
include: { items: true },
})
// Verifica disponibilita' per ogni articolo
for (const item of order.items) {
const product = await tx.product.findUniqueOrThrow({
where: { id: item.productId },
})
if (product.stock < item.quantity) {
throw new Error(`Prodotto ${product.name}: stock insufficiente`)
}
// Decrementa lo stock
await tx.product.update({
where: { id: product.id },
data: { stock: { decrement: item.quantity } },
})
}
// Aggiorna lo stato dell'ordine
return tx.order.update({
where: { id: orderId },
data: { status: 'PROCESSING' },
})
})
return result
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
console.error('Errore Prisma:', error.code, error.message)
} else {
console.error('Errore nella transazione:', error)
}
throw error
}
}
Retry Pattern
Per gestire errori transitori (deadlock, timeout):
async function withRetry<T>(
fn: () => Promise<T>,
maxRetries: number = 3
): Promise<T> {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
return await fn()
} catch (error) {
if (attempt === maxRetries) throw error
const isRetryable =
error instanceof Prisma.PrismaClientKnownRequestError &&
error.code === 'P2034' // Transaction conflict
if (!isRetryable) throw error
// Attesa esponenziale
await new Promise((r) => setTimeout(r, 100 * Math.pow(2, attempt)))
}
}
throw new Error('Max retries raggiunto')
}
// Uso
const result = await withRetry(() =>
prisma.$transaction(async (tx) => {
// ... operazioni
})
)