Destino Dev

Blog

Transações no PostgreSQL com Prisma: commit, rollback e locks passo a passo

Por John Pires · 08 de março de 2026

Contexto: quando voce vai usar isso

Por que transacoes importam

Transacoes sao a base para garantir integridade de dados em bancos relacionais. Quando voce atualiza multiplas linhas em tabelas diferentes e depende de todas essas alteracoes acontecerem juntas, as transacoes fazem o trabalho pesado. Sem elas, falhas no meio do processo podem deixar o banco em estado inconsistente.

Imagine um sistema de transferencias entre contas. Debitar uma conta sem creditar a outra e um erro comum sem transacao. Transacoes proporcionam as promessas basicas: atomicidade, consistencia, isolamento e durabilidade, conhecidas como ACID.

Cenarios tipicos no dia a dia

Voce vai usar transacoes ao implementar:

  • Transferencias financeiras entre contas
  • Operacoes de inventario onde existe decremento e log simultaneo
  • Operacoes de migracao ou seeds que precisam ser um unico passo
  • Operacoes que requerem locks para evitar race conditions

No ecossistema Node com Prisma e Postgres, as transacoes sao acessiveis via prisma client. Saber quando e como usar commit e rollback evita perda de dados e bugs sutis.

Explicacao visual com diagramas mentais

Analogia: caixa registradora

Pense numa transacao como uma caixa registradora em que voce registra tentativas antes de fechar a gaveta. Enquanto a gaveta nao é fechada, ainda e possivel desfazer as operacoes. Ao fechar, todas as operacoes sao confirmadas de uma vez so. Se houver um problema, voce abre a gaveta e volta atraz.

Essa analogia ajuda a entender o commit e o rollback: commit = fechar gaveta; rollback = desfazer tudo antes de fechar.

Fluxo passo a passo com diagrama

Fluxo simplificado de uma transacao

Cliente inicia acao
  |
  V
  Banco inicia transacao
  |
  V
  Operacoes DML (insert, update, delete)
  |
  V
  se tudo ok -> commit (tudo persiste)
  se erro -> rollback (nenhuma alteracao persiste)

Adicione locks ao desenho quando varias transacoes concorrem sobre os mesmos registros. Locks evitam que duas caixas registradoras mexam na mesma nota ao mesmo tempo.

Regra pratica: use transacoes curtas. Quanto mais tempo mantiver uma transacao aberta, maior o risco de locks e impasses na concorrencia.

Primeiro exemplo simples

Prisma schema e setup

Antes de qualquer codigo, voce precisa de um modelo no arquivo prisma schema. Exemplo basico de contas para praticar transacoes e lock de saldo.

Este bloco mostra um modelo simples com duas tabelas relevantes para o exemplo: Account e TransactionLog. Use esse schema em um projeto prisma ja inicializado.

// prisma schema - modelo basico de contas
// o arquivo deve ser salvo em prisma/schema.prisma

datasource db {
  provider = "postgresql"
  url      = env('DATABASE_URL')
}

generator client {
  provider = "prisma-client-js"
}

model Account {
  id       Int    @id @default(autoincrement())
  owner    String
  balance  Decimal @db.Decimal(18, 2)
  createdAt DateTime @default(now())
}

model TransactionLog {
  id        Int      @id @default(autoincrement())
  fromId    Int
  toId      Int
  amount    Decimal  @db.Decimal(18, 2)
  createdAt DateTime @default(now())
}

Depois de criar o schema, rode prisma migrate dev para aplicar as tabelas no seu banco Postgres. Esse modelo permite operações de debito/credito e log das transferencias.

Transacao basica com commit

Este script Node demonstra uma transacao simples usando prisma client e o atalho para executar multiplas promises atomicas. Use quando cada operacao ja eh isolada e nao depende de leitura lockada.

// exemplo Node - transacao basica com commit
// o que faz: debita e credita duas contas, e registra o log

import { PrismaClient } from '@prisma/client'
const prisma = new PrismaClient()

async function transferSimple(fromId, toId, amount) {
  await prisma.$transaction([
    prisma.account.update({ where: { id: fromId }, data: { balance: { decrement: amount } } }),
    prisma.account.update({ where: { id: toId }, data: { balance: { increment: amount } } }),
    prisma.transactionLog.create({ data: { fromId, toId, amount } })
  ])
}

// o que acontece: se todas as operacoes retornarem com sucesso, o commit eh automatico
// se alguma falhar, prisma faz rollback e o banco fica como antes

Esse padrao em array funciona bem quando as operacoes sao simples e isoladas. Prisma tenta executar as promises dentro de uma transacao e faz commit ou rollback automaticamente.

Segundo exemplo mais realista

Transferencia entre contas com rollback explicito

Quando a logica exige leitura de saldo antes de atualizar, e necessario usar a transacao interativa que fornece um client transacional. Assim garantimos que a leitura e a escrita acontecem no mesmo contexto transacional.

Exemplo a seguir demonstra uma transferencia com verificacao de saldo e tratamento de erro que causa rollback.

// transferencia segura com client transacional
// o que faz: le saldo, verifica saldo, debita, credita e registra log

import { PrismaClient } from '@prisma/client'
const prisma = new PrismaClient()

async function transferWithCheck(fromId, toId, amount) {
  return prisma.$transaction(async tx => {
    const from = await tx.account.findUnique({ where: { id: fromId } })
    if (!from) throw new Error('conta de origem nao encontrada')

    if (from.balance < amount) throw new Error('saldo insuficiente')

    await tx.account.update({ where: { id: fromId }, data: { balance: { decrement: amount } } })
    await tx.account.update({ where: { id: toId }, data: { balance: { increment: amount } } })
    await tx.transactionLog.create({ data: { fromId, toId, amount } })

    return { status: 'ok' }
  })
}

// o que acontece: se lancar erro antes do fim, prisma faz rollback automatico e nada sera persistido

Neste padrao, o parametro tx eh um prisma client transacional. Todas as operacoes feitas com tx participam da mesma transacao e serao confirmadas apenas no final do callback.

Bloqueio de linha com SELECT FOR UPDATE

Para evitar race conditions quando varias transacoes leem e atualizam o mesmo saldo ao mesmo tempo, voce pode usar um lock de linha. Em Postgres, SELECT FOR UPDATE bloqueia a linha ate o commit ou rollback.

Prisma nao expoe metodo especifico para FOR UPDATE nas funcoes de alto nivel, entao usamos query raw dentro da mesma transacao para adquirir o lock, e em seguida atualizamos via tx client.

// uso de SELECT FOR UPDATE dentro da transacao
// o que faz: trava a linha da conta de origem, garante leitura consistente

import { PrismaClient } from '@prisma/client'
const prisma = new PrismaClient()

async function transferWithLock(fromId, toId, amount) {
  return prisma.$transaction(async tx => {
    // trava a linha da conta de origem
    const rows = await tx.$queryRaw`SELECT id, balance FROM account WHERE id = ${fromId} FOR UPDATE`
    const from = rows[0]
    if (!from) throw new Error('conta de origem nao encontrada')

    if (from.balance < amount) throw new Error('saldo insuficiente')

    await tx.account.update({ where: { id: fromId }, data: { balance: { decrement: amount } } })
    await tx.account.update({ where: { id: toId }, data: { balance: { increment: amount } } })
    await tx.transactionLog.create({ data: { fromId, toId, amount } })

    return { status: 'ok' }
  })
}

// o que acontece: durante a transacao, a linha fica bloqueada, evitando que outra transacao modifique o mesmo registro

Esse padrao reduz inconsistencias em cenarios de alta concorrencia, mas aumenta o tempo de bloqueio. Use com criterio.

Erros comuns de iniciante e como evitar

Abrir transacao por muito tempo

Erro comum: iniciar uma transacao e realizar operacoes lentas ou chamadas externas dentro dela, por exemplo chamadas HTTP. Isso expande a janela de lock e aumenta riscos de deadlock e timeouts.

Evite fazer I O de rede dentro de uma transacao. Se confirmar necessario, a estrategia eh colher dados fora da transacao e depois aplicar atualizacoes pequenas e rapidas dentro da transacao.

Usar o client global em vez do client transacional

Muitos iniciantes esquecem que, dentro de prisma.$transaction callback, eh preciso usar o client passado ao callback (tx). Usar o prisma global quebra o contrato e causa leituras fora da transacao.

// exemplo errado - nao use prisma global dentro da transacao
// o que faz: pode causar leituras sujas e comportamento inesperado

import { PrismaClient } from '@prisma/client'
const prisma = new PrismaClient()

async function wrongExample(fromId, toId, amount) {
  return prisma.$transaction(async tx => {
    // erro: usando prisma global em vez de tx
    const from = await prisma.account.findUnique({ where: { id: fromId } })
    if (from.balance < amount) throw new Error('saldo insuficiente')
    await tx.account.update({ where: { id: fromId }, data: { balance: { decrement: amount } } })
    await tx.account.update({ where: { id: toId }, data: { balance: { increment: amount } } })
  })
}

// o que acontece: a leitura inicial pode nao estar isolada na transacao e levar a inconsistencias

Solucao: sempre use o tx recebido para todas as operacoes dentro do callback.

Outras armadilhas e dicas praticas

  • Timeout de transacao - configure timeouts de conexao e tenha retries para operacoes que podem sofrer deadlock
  • Deadlocks - garanta ordenacao consistente de locks quando atualizar multiplas linhas para reduzir riscos
  • Log e monitoramento - registre falhas de transacao para detectar padroes e casos de concorrencia intensa
Dica rapida: implemente retries exponenciais para operacoes que falham por deadlock, mas limite o numero de tentativas para evitar loops infinitos.

Conclusao pratica

Checklist rapido

Ao implementar transacoes com Prisma e Postgres siga este checklist simples:

  1. Modelar o banco com tipos adequados e precision para valores monetarios
  2. Usar prisma.$transaction com array para batches simples ou callback para logica que precisa de leituras
  3. Usar tx client recebido no callback para todas as consultas dentro da transacao
  4. Evitar I O demorado dentro da transacao e preferir locks curtos
  5. Considerar SELECT FOR UPDATE via query raw quando precisar de locks de linha

Proximos passos

Pratique com pequenos scripts de transferencia em ambiente local. Em seguida, simule concorrencia com multiplos workers para observar comportamentos de lock e deadlock. Aprenda a interpretar logs do Postgres com pg_stat_activity para identificar transacoes longas.

Dominar transacoes e locks eh essencial para construir aplicacoes confiaveis que escalam. Use os exemplos deste artigo como base e adapte as estrategias a sua carga e padrao de acesso.

Boa pratica: mantenha os testes automatizados que simulam casos de concorrencia. Eles serao o indicador mais rapido de problemas em logica transacional.

Assuntos: PostgreSQL, Prisma, transacoes, rollback, locks, Node.js

Leia também