Transações são um conceito fundamental em bancos de dados relacionais, permitindo agrupar múltiplas operações SQL em uma única unidade lógica que é executada por completo ou não é executada (atomicidade). Com o PDO (PHP Data Objects), você pode gerenciar transações de forma simples e segura, garantindo a integridade dos dados mesmo em cenários de falhas.

Nesta aula, exploraremos os métodos beginTransaction(), commit() e rollback() da classe PDO, discutiremos quando é apropriado usar transações, como lidar com erros e veremos exemplos completos de código. Ao final, você estará apto a implementar transações robustas em suas aplicações PHP.

beginTransaction, commit, rollback

O PDO oferece três métodos principais para gerenciar transações:

  • beginTransaction(): desliga o modo de autocommit e inicia uma transação. A partir deste ponto, todas as instruções SQL são executadas dentro da transação até que commit() ou rollback() sejam chamados.
  • commit(): confirma todas as operações realizadas desde o início da transação, tornando-as permanentes no banco de dados.
  • rollback(): desfaz todas as operações realizadas desde o início da transação, revertendo o banco ao estado anterior.

É importante notar que nem todos os bancos de dados ou engines suportam transações (ex.: MyISAM no MySQL não suporta). Além disso, após chamar rollback() ou commit(), a conexão retorna ao modo de autocommit automaticamente.

Exemplo básico:

try {
    $pdo = new PDO('mysql:host=localhost;dbname=teste', 'usuario', 'senha');
    $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

    $pdo->beginTransaction();
    $pdo->exec("INSERT INTO contas (nome, saldo) VALUES ('Alice', 1000)");
    $pdo->exec("INSERT INTO contas (nome, saldo) VALUES ('Bob', 500)");
    $pdo->commit();
} catch (Exception $e) {
    $pdo->rollback();
    echo "Falha: " . $e->getMessage();
}

Quando usar

Transações são indicadas sempre que você precisa executar múltiplas operações que dependem umas das outras. Exemplos comuns:

  • Transferências bancárias: debitar de uma conta e creditar em outra. Se uma falha ocorrer, ambas devem ser desfeitas.
  • Pedidos de compra: inserir o pedido, os itens e atualizar o estoque. Qualquer erro deve reverter todo o processo.
  • Operações que exigem consistência: se uma consulta intermediária falhar, todo o bloco deve ser descartado.

Evite usar transações para operações únicas e simples, pois o overhead de gerenciamento pode ser desnecessário. Além disso, transações longas podem causar bloqueios e reduzir a concorrência no banco.

Tratamento de erros

O tratamento de erros é crucial em transações. A abordagem recomendada é usar exceções (modo ERRMODE_EXCEPTION) e um bloco try-catch para capturar qualquer falha e chamar rollback().

Exemplo com tratamento robusto:

try {
    $pdo = new PDO('mysql:host=localhost;dbname=teste', 'usuario', 'senha');
    $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

    $pdo->beginTransaction();

    $stmt = $pdo->prepare("UPDATE contas SET saldo = saldo - ? WHERE id = ?");
    $stmt->execute([100, 1]);

    $stmt = $pdo->prepare("UPDATE contas SET saldo = saldo + ? WHERE id = ?");
    $stmt->execute([100, 2]);

    $pdo->commit();
} catch (PDOException $e) {
    $pdo->rollback();
    // Log do erro ou mensagem amigável
    echo "Erro na transação: " . $e->getMessage();
}

Lembre-se de que rollback() só deve ser chamado se a transação estiver ativa. Você pode verificar com $pdo->inTransaction().

Exemplos

Vamos a um exemplo completo de transferência bancária com validação de saldo:

try {
    $pdo = new PDO('mysql:host=localhost;dbname=banco', 'root', '');
    $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

    $contaOrigem = 1;
    $contaDestino = 2;
    $valor = 200;

    $pdo->beginTransaction();

    // Verificar saldo
    $stmt = $pdo->prepare("SELECT saldo FROM contas WHERE id = ? FOR UPDATE");
    $stmt->execute([$contaOrigem]);
    $saldo = $stmt->fetchColumn();

    if ($saldo < $valor) {
        throw new Exception("Saldo insuficiente");
    }

    // Debitar
    $stmt = $pdo->prepare("UPDATE contas SET saldo = saldo - ? WHERE id = ?");
    $stmt->execute([$valor, $contaOrigem]);

    // Creditar
    $stmt = $pdo->prepare("UPDATE contas SET saldo = saldo + ? WHERE id = ?");
    $stmt->execute([$valor, $contaDestino]);

    $pdo->commit();
    echo "Transferência realizada com sucesso.";
} catch (Exception $e) {
    $pdo->rollback();
    echo "Falha na transferência: " . $e->getMessage();
}

Note o uso de FOR UPDATE para bloquear a linha durante a transação, evitando condições de corrida. Esse é um padrão importante em sistemas concorrentes.

Boas práticas

  • Sempre configure o modo de erro para exceções (ERRMODE_EXCEPTION).
  • Mantenha as transações curtas para evitar locks prolongados.
  • Evite interações com o usuário ou operações lentas dentro da transação.
  • Use prepared statements para prevenir SQL injection.
  • Considere o uso de savepoints para transações aninhadas (se o banco suportar).

Referências

Exercícios

  1. Crie uma transação que insira um novo usuário e um perfil associado em duas tabelas diferentes (usuarios e perfis). Use rollback se algo falhar.

    ✓ Resposta:
    try {
        $pdo = new PDO('mysql:host=localhost;dbname=teste', 'root', '');
        $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
        $pdo->beginTransaction();
        $stmt = $pdo->prepare("INSERT INTO usuarios (nome, email) VALUES (?, ?)");
        $stmt->execute(['João', 'joao@email.com']);
        $userId = $pdo->lastInsertId();
        $stmt = $pdo->prepare("INSERT INTO perfis (usuario_id, bio) VALUES (?, ?)");
        $stmt->execute([$userId, 'Bio do João']);
        $pdo->commit();
    } catch (Exception $e) {
        $pdo->rollback();
        echo "Erro: " . $e->getMessage();
    }
  2. Implemente uma transferência bancária que verifica se o saldo é suficiente antes de debitar. Se não for, lance uma exceção personalizada e faça rollback.

    ✓ Resposta:
    class SaldoInsuficienteException extends Exception {}
    try {
        $pdo = new PDO('mysql:host=localhost;dbname=banco', 'root', '');
        $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
        $pdo->beginTransaction();
        $stmt = $pdo->prepare("SELECT saldo FROM contas WHERE id = ? FOR UPDATE");
        $stmt->execute([1]);
        $saldo = $stmt->fetchColumn();
        if ($saldo < 100) {
            throw new SaldoInsuficienteException("Saldo insuficiente");
        }
        $pdo->prepare("UPDATE contas SET saldo = saldo - 100 WHERE id = 1")->execute();
        $pdo->prepare("UPDATE contas SET saldo = saldo + 100 WHERE id = 2")->execute();
        $pdo->commit();
    } catch (SaldoInsuficienteException $e) {
        $pdo->rollback();
        echo $e->getMessage();
    } catch (Exception $e) {
        $pdo->rollback();
        echo "Erro geral: " . $e->getMessage();
    }
  3. Escreva um código que use savepoints para permitir rollback parcial dentro de uma transação.

    ✓ Resposta:
    try {
        $pdo = new PDO('mysql:host=localhost;dbname=teste', 'root', '');
        $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
        $pdo->beginTransaction();
        $pdo->exec("INSERT INTO logs (mensagem) VALUES ('Início')");
        $pdo->exec("SAVEPOINT ponto1");
        $pdo->exec("INSERT INTO logs (mensagem) VALUES ('Ponto 1')");
        // Se algo der errado, podemos reverter até o savepoint
        $pdo->exec("ROLLBACK TO SAVEPOINT ponto1");
        $pdo->exec("INSERT INTO logs (mensagem) VALUES ('Após rollback parcial')");
        $pdo->commit();
    } catch (Exception $e) {
        $pdo->rollback();
        echo "Erro: " . $e->getMessage();
    }
  4. Explique o que acontece se você chamar commit() duas vezes seguidas sem uma nova transação.

    ✓ Resposta:Chamar commit() quando não há transação ativa lançará uma exceção PDOException com a mensagem "There is no active transaction". Portanto, é importante verificar com inTransaction() ou garantir que o fluxo sempre tenha uma transação aberta.
  5. Crie uma função que execute múltiplas queries em uma transação e retorne true em sucesso ou false em falha (sem exceções).

    ✓ Resposta:
    function executarTransacao(PDO $pdo, array $queries): bool {
        try {
            $pdo->beginTransaction();
            foreach ($queries as $query) {
                $pdo->exec($query);
            }
            $pdo->commit();
            return true;
        } catch (Exception $e) {
            $pdo->rollback();
            return false;
        }
    }
    // Uso:
    $queries = [
        "UPDATE contas SET saldo = saldo - 100 WHERE id = 1",
        "UPDATE contas SET saldo = saldo + 100 WHERE id = 2"
    ];
    $resultado = executarTransacao($pdo, $queries);
    if ($resultado) {
        echo "Sucesso";
    } else {
        echo "Falha";
    }