00
:
00
:
00
:
00
Corso SEO AI - Usa SEOEMAIL al checkout per il 30% di sconto

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