Transactions: COMMIT, ROLLBACK e SAVEPOINT

1. Introdução às Transações no Banco de Dados

Uma transação é uma unidade lógica de trabalho composta por uma ou mais operações SQL que devem ser executadas de forma atômica — ou todas as operações são concluídas com sucesso, ou nenhuma delas surte efeito permanente no banco de dados. Esse conceito é fundamental para garantir a integridade dos dados em sistemas críticos.

As transações obedecem ao princípio ACID:
- Atomicidade: a transação é executada por completo ou é totalmente revertida.
- Consistência: o banco de dados passa de um estado válido para outro estado válido.
- Isolamento: transações concorrentes não interferem entre si.
- Durabilidade: uma vez confirmada, a transação persiste mesmo em caso de falha.

Cenário clássico: transferência bancária entre duas contas. Debitar R$ 500,00 da conta A e creditar R$ 500,00 na conta B são duas operações que devem ocorrer juntas. Se uma falha acontecer após o débito e antes do crédito, o sistema precisaria reverter o débito — para isso servem as transações.

2. Iniciando uma Transação e o Comando COMMIT

Para iniciar uma transação, utiliza-se BEGIN ou START TRANSACTION. Após executar as operações desejadas, o comando COMMIT confirma todas as alterações permanentemente.

BEGIN;

INSERT INTO pedidos (id_cliente, data_pedido, valor_total)
VALUES (101, CURRENT_DATE, 1500.00);

INSERT INTO itens_pedido (id_pedido, id_produto, quantidade, preco_unitario)
VALUES (LASTVAL(), 45, 2, 500.00);

INSERT INTO itens_pedido (id_pedido, id_produto, quantidade, preco_unitario)
VALUES (LASTVAL(), 78, 1, 500.00);

COMMIT;

Neste exemplo, um pedido e seus itens são inseridos em uma única transação. O COMMIT garante que, se todas as inserções forem bem-sucedidas, os dados serão gravados definitivamente. Caso contrário, nenhum registro é persistido.

3. Desfazendo Alterações com ROLLBACK

O comando ROLLBACK reverte todas as alterações feitas desde o início da transação, restaurando o banco ao estado anterior ao BEGIN.

BEGIN;

UPDATE contas SET saldo = saldo - 500 WHERE id_conta = 1;
UPDATE contas SET saldo = saldo + 500 WHERE id_conta = 2;

-- Verificação de consistência: saldo da conta 1 não pode ficar negativo
SELECT saldo FROM contas WHERE id_conta = 1;
-- Suponha que o saldo era 300 e ficou -200 (erro!)

ROLLBACK;
-- As duas operações são desfeitas, o banco volta ao estado original

Se qualquer operação dentro da transação falhar ou violar regras de negócio, o ROLLBACK evita que dados inconsistentes sejam salvos. Isso é essencial em operações financeiras, reservas e qualquer processo que exija integridade referencial.

4. Controle Granular com SAVEPOINT

Nem sempre é necessário reverter toda a transação. Com SAVEPOINT, é possível marcar pontos intermediários e reverter apenas parte do trabalho executado.

BEGIN;

INSERT INTO log_processamento (mensagem) VALUES ('Iniciando lote 1');
SAVEPOINT lote1;

INSERT INTO registros (dado) VALUES ('A');
INSERT INTO registros (dado) VALUES ('B');
-- Erro detectado no lote 1

ROLLBACK TO SAVEPOINT lote1;
-- As inserções de A e B são desfeitas, mas o log permanece

INSERT INTO registros (dado) VALUES ('C');
INSERT INTO registros (dado) VALUES ('D');

SAVEPOINT lote2;
INSERT INTO registros (dado) VALUES ('E');
-- Sucesso no lote 2

RELEASE SAVEPOINT lote2;
-- Libera o ponto de salvamento (opcional, ocorre automaticamente no COMMIT)

COMMIT;

O RELEASE SAVEPOINT remove o ponto de salvamento sem afetar as operações realizadas. É uma boa prática liberar SAVEPOINTs que não serão mais utilizados.

5. Transações Aninhadas e Comportamento do PostgreSQL

Bancos relacionais tratam transações aninhadas de formas diferentes. O PostgreSQL, por exemplo, não suporta transações aninhadas verdadeiras, mas simula esse comportamento com SAVEPOINT.

BEGIN;
  INSERT INTO log VALUES ('Transação principal iniciada');

  SAVEPOINT ponto_interno;
  INSERT INTO dados VALUES (1);
  ROLLBACK TO SAVEPOINT ponto_interno;
  -- O INSERT acima é desfeito

  SAVEPOINT ponto_interno2;
  INSERT INTO dados VALUES (2);
  -- Mantido

COMMIT;
-- Apenas o valor 2 e o log são persistidos

Diferenças entre bancos:
- PostgreSQL: usa SAVEPOINT para simular aninhamento. ROLLBACK sem especificar TO SAVEPOINT reverte toda a transação.
- MySQL (InnoDB): suporta SAVEPOINT de forma similar, mas ROLLBACK em transações aninhadas pode ter comportamento diferente dependendo da versão.
- Oracle: permite transações aninhadas reais com SAVEPOINT, mas o ROLLBACK sempre reverte até o SAVEPOINT especificado ou até o início da transação.

6. Boas Práticas e Armadilhas Comuns

Evite transações longas: enquanto uma transação está ativa, locks podem ser mantidos em linhas ou tabelas, bloqueando outras operações e reduzindo a concorrência.

-- Ruim: transação longa com pausa manual
BEGIN;
  UPDATE contas SET saldo = saldo - 100 WHERE id = 1;
  -- Aqui o usuário pensa por 5 minutos...
COMMIT; -- Lock mantido por muito tempo

Cuidado com DDL dentro de transações: comandos como CREATE TABLE, ALTER TABLE e DROP TABLE (DDL) geralmente executam um COMMIT implícito antes e depois de sua execução, encerrando a transação atual.

BEGIN;
  INSERT INTO log VALUES ('teste');
  CREATE TABLE temp (id INT); -- COMMIT implícito aqui!
  INSERT INTO log VALUES ('outro'); -- Nova transação
COMMIT;
-- O primeiro INSERT foi confirmado automaticamente

Tratamento de erros com stored procedures: em funções ou procedimentos armazenados, utilize blocos EXCEPTION para capturar erros e decidir entre ROLLBACK parcial ou total.

Deadlocks: transações que atualizam registros em ordens diferentes podem gerar deadlocks. Sempre acesse tabelas e linhas em uma ordem consistente.

-- Transação 1: atualiza A depois B
BEGIN; UPDATE contas SET saldo = saldo - 10 WHERE id = 1; UPDATE contas SET saldo = saldo + 10 WHERE id = 2; COMMIT;

-- Transação 2: atualiza B depois A (potencial deadlock)
BEGIN; UPDATE contas SET saldo = saldo - 10 WHERE id = 2; UPDATE contas SET saldo = saldo + 10 WHERE id = 1; COMMIT;

7. Exemplo Completo: Sistema de Reservas

O cenário abaixo simula um sistema de reserva de assentos para um evento, onde o usuário pode tentar reservar múltiplos assentos, mas deseja confirmar apenas se todos estiverem disponíveis.

-- Início da transação
BEGIN;

-- Inserir registro da reserva
INSERT INTO reservas (id_usuario, data_reserva, status)
VALUES (42, CURRENT_TIMESTAMP, 'pendente');

-- Obter o ID da reserva recém-criada
-- (em PostgreSQL, usar RETURNING ou CURRVAL)
SAVEPOINT reserva_criada;

-- Tentar reservar assento 1
UPDATE assentos SET id_reserva = LASTVAL(), status = 'ocupado'
WHERE numero = 10 AND id_evento = 5 AND status = 'livre';

IF NOT FOUND THEN
    ROLLBACK TO SAVEPOINT reserva_criada;
    -- Assento 1 indisponível, reverter tudo
    ROLLBACK;
END IF;

-- Tentar reservar assento 2
SAVEPOINT assento2;
UPDATE assentos SET id_reserva = LASTVAL(), status = 'ocupado'
WHERE numero = 11 AND id_evento = 5 AND status = 'livre';

IF NOT FOUND THEN
    ROLLBACK TO SAVEPOINT reserva_criada;
    -- Assento 2 indisponível, reverter toda a reserva
    ROLLBACK;
END IF;

-- Tentar reservar assento 3
SAVEPOINT assento3;
UPDATE assentos SET id_reserva = LASTVAL(), status = 'ocupado'
WHERE numero = 12 AND id_evento = 5 AND status = 'livre';

IF NOT FOUND THEN
    ROLLBACK TO SAVEPOINT reserva_criada;
    ROLLBACK;
END IF;

-- Todos os assentos reservados com sucesso
UPDATE reservas SET status = 'confirmada' WHERE id = LASTVAL();

RELEASE SAVEPOINT assento3;
RELEASE SAVEPOINT assento2;
RELEASE SAVEPOINT reserva_criada;

COMMIT;
-- Apenas se todos os assentos estiverem disponíveis a reserva é confirmada

Neste exemplo, se qualquer assento estiver ocupado, toda a transação é revertida. O uso de SAVEPOINT permite reverter apenas a tentativa atual e continuar, mas a lógica de negócio optou por reverter tudo para garantir atomicidade.

Referências