Módulo 2 · SQL Server / Procedures — Capítulo 05

Transações — BEGIN/COMMIT/ROLLBACK

O que garante que "ou tudo acontece, ou nada acontece" dentro de uma procedure — e o detalhe (XACT_ABORT) que a maioria dos tutoriais esquece de mencionar.

1. O problema que transações resolvem

Imagine uma transferência bancária: debitar R$ 100 da conta A e creditar R$ 100 na conta B. São dois comandos UPDATE. Se o primeiro rodar e o segundo falhar (queda de conexão, violação de constraint, o que for), o dinheiro simplesmente desaparece — saiu da conta A e não chegou a lugar nenhum. Uma transação existe para impedir exatamente isso.

ACID, em uma frase cada

Atomicidade: tudo ou nada. Consistência: o banco sai de um estado válido para outro estado válido. Isolamento: transações concorrentes não se enxergam no meio do caminho. Durabilidade: uma vez commitada, a mudança sobrevive mesmo a uma queda de energia. SQL Server garante as quatro — a sua parte é usar BEGIN TRANSACTION corretamente.

2. Sintaxe básica

SQL transferência entre contas — versão ingênua
CREATE PROCEDURE dbo.usp_ContaTransferir
    @ContaOrigemId  INT,
    @ContaDestinoId INT,
    @Valor          DECIMAL(10,2)
AS
BEGIN
    SET NOCOUNT ON;

    BEGIN TRANSACTION;

    UPDATE dbo.Conta SET Saldo = Saldo - @Valor WHERE Id = @ContaOrigemId;
    UPDATE dbo.Conta SET Saldo = Saldo + @Valor WHERE Id = @ContaDestinoId;

    COMMIT TRANSACTION;
END;
GO

BEGIN TRANSACTION abre um "envelope": nada dentro dele é gravado de forma definitiva até COMMIT TRANSACTION. Se algo der errado antes do COMMIT, ROLLBACK TRANSACTION desfaz tudo que aconteceu desde o BEGIN.

O problema desta versão

Ela não trata erro nenhum. Se o segundo UPDATE falhar (por exemplo, @ContaDestinoId não existir e violar uma constraint), o SQL Server por padrão não desfaz automaticamente o primeiro UPDATE — a transação fica aberta, travando linhas, até alguém decidir o que fazer. Isso é resolvido na próxima seção.

3. TRY/CATCH + XACT_ABORT — a versão de produção

A combinação correta usa três peças: SET XACT_ABORT ON, TRY/CATCH (aprofundado no próximo capítulo) e ROLLBACK dentro do CATCH.

SQL transferência entre contas — versão correta
CREATE PROCEDURE dbo.usp_ContaTransferir
    @ContaOrigemId  INT,
    @ContaDestinoId INT,
    @Valor          DECIMAL(10,2)
AS
BEGIN
    SET NOCOUNT ON;
    SET XACT_ABORT ON;

    BEGIN TRY
        BEGIN TRANSACTION;

        UPDATE dbo.Conta SET Saldo = Saldo - @Valor WHERE Id = @ContaOrigemId;
        UPDATE dbo.Conta SET Saldo = Saldo + @Valor WHERE Id = @ContaDestinoId;

        COMMIT TRANSACTION;
    END TRY
    BEGIN CATCH
        IF @@TRANCOUNT > 0
            ROLLBACK TRANSACTION;

        THROW;
    END CATCH
END;
GO
BEGIN TRANSACTION UPDATE Conta (origem) UPDATE Conta (destino) deu erro tudo ok ROLLBACK COMMIT nada foi gravado tudo foi gravado

Fig. 1 — Toda transação termina de uma das duas formas: as duas UPDATEs valem, ou nenhuma vale.

Explicando cada peça nova:

ComandoPapel
SET XACT_ABORT ONFaz o SQL Server abortar e desfazer a transação automaticamente diante de qualquer erro de execução — sem isso, alguns erros deixam a transação aberta.
@@TRANCOUNTConta quantas transações estão abertas no momento. Checar antes do ROLLBACK evita erro "sem transação para desfazer".
THROWRelança o erro original para quem chamou a procedure, depois do ROLLBACK — assunto aprofundado no capítulo 6.
Dica

Adote SET NOCOUNT ON; SET XACT_ABORT ON; como as duas primeiras linhas de toda procedure que faz alguma escrita (INSERT/UPDATE/DELETE). É o tipo de hábito que evita um incidente de dados inconsistentes meses depois.

4. Transações aninhadas e @@TRANCOUNT

T-SQL permite "aninhar" BEGIN TRANSACTION, mas isso é enganoso: só a transação mais externa realmente conta. Um COMMIT interno apenas decrementa o contador (@@TRANCOUNT); só o COMMIT que zera o contador grava de fato. Já um ROLLBACK, em qualquer nível, desfaz tudo, até a mais externa.

SQL SAVE TRANSACTION — desfazer só uma parte
BEGIN TRANSACTION;

    UPDATE dbo.Conta SET Saldo = Saldo - 100 WHERE Id = 1;

    SAVE TRANSACTION PontoIntermediario;

    UPDATE dbo.Conta SET Saldo = Saldo + 100 WHERE Id = 999; -- conta não existe

    IF @@ROWCOUNT = 0
        ROLLBACK TRANSACTION PontoIntermediario; -- desfaz só o segundo UPDATE

COMMIT TRANSACTION; -- o débito da conta 1 é mantido

SAVE TRANSACTION cria um ponto de retorno parcial — útil, mas raro no dia a dia. Na prática, a grande maioria das procedures usa uma única transação simples como no exemplo da seção 3.

5. Onde isso te leva

Você já sabe blindar uma procedure contra falhas parciais. O que falta é entender TRY/CATCH em profundidade — capturar o erro, ler os detalhes (ERROR_MESSAGE(), ERROR_LINE()...) e decidir o que relançar para a aplicação C#. É o próximo capítulo.

📌 Resumo do capítulo

  • Transação = "tudo ou nada": BEGIN TRANSACTION ... COMMIT/ROLLBACK.
  • SET XACT_ABORT ON é essencial — sem ele, alguns erros não desfazem a transação automaticamente.
  • Padrão de produção: TRY com BEGIN TRANSACTION/COMMIT, CATCH com checagem de @@TRANCOUNT + ROLLBACK + THROW.
  • SAVE TRANSACTION permite desfazer só uma parte de uma transação maior.

✏️ Praticando

  1. Crie a tabela Conta (Id, Titular, Saldo) e implemente usp_ContaTransferir como no exemplo da seção 3.
  2. Teste transferindo para um @ContaDestinoId inexistente e confirme, consultando a tabela, que o saldo da origem não foi alterado.
  3. Adicione uma verificação de saldo insuficiente antes do primeiro UPDATE, lançando um erro (você vai formalizar isso com THROW no próximo capítulo — por ora, use RAISERROR).