Transações são fundamentais para garantir a consistência dos dados em operações que envolvem múltiplas consultas SQL. Com o PDO, é possível gerenciar transações de forma simples e eficiente, utilizando os métodos beginTransaction(), commit() e rollback(). Esta aula explora como implementar transações em PHP, quando utilizá-las e como tratar erros adequadamente.

Uma transação agrupa uma ou mais operações de banco de dados em uma única unidade lógica. Se todas as operações forem bem-sucedidas, a transação é confirmada (commit); caso contrário, todas as alterações são desfeitas (rollback). Isso evita que dados fiquem em estado inconsistente, como em transferências bancárias ou inserções em múltiplas tabelas.

beginTransaction, commit, rollback

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

  • beginTransaction(): inicia uma transação. A partir deste ponto, todas as consultas são executadas em modo transacional até que commit() ou rollback() seja chamado.
  • 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, restaurando o estado anterior.

É importante verificar se o banco de dados suporta transações (ex.: MySQL com InnoDB, PostgreSQL). O PDO também oferece o método inTransaction() que retorna true se uma transação estiver ativa.

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

    $pdo->beginTransaction();

    // Operações SQL
    $pdo->exec("INSERT INTO contas (nome, saldo) VALUES ('João', 1000)");
    $pdo->exec("UPDATE contas SET saldo = saldo - 100 WHERE nome = 'Maria'");

    $pdo->commit();
} catch (Exception $e) {
    $pdo->rollback();
    echo "Erro na transação: " . $e->getMessage();
}

Quando usar

Transações são necessárias sempre que a integridade dos dados depende de múltiplas operações serem executadas atomicamente. Exemplos comuns incluem:

  • Transferências bancárias: debitar de uma conta e creditar em outra.
  • Cadastro de pedidos: inserir pedido e itens do pedido.
  • Operações que envolvem verificação de saldo ou estoque.
  • Inserções em tabelas relacionadas que exigem consistência.

Evite usar transações para operações simples e independentes, pois elas adicionam overhead. Além disso, transações longas podem causar bloqueios no banco de dados, então mantenha-as curtas e rápidas.

Tratamento de erros

O tratamento de erros em transações deve garantir que qualquer falha resulte em rollback. A melhor prática é usar um bloco try-catch e chamar rollback() dentro do catch. É crucial configurar o modo de erro do PDO para exceções (PDO::ERRMODE_EXCEPTION) para que os erros sejam capturados adequadamente.

Além disso, é possível verificar o sucesso de cada operação individualmente, mas o uso de exceções simplifica o código. Cuidado com transações aninhadas: o PDO não suporta nativamente, mas você pode simular com savepoints.

try {
    $pdo->beginTransaction();
    // operações
    $pdo->commit();
} catch (PDOException $e) {
    $pdo->rollback();
    // log do erro
    error_log($e->getMessage());
    echo "Transação falhou. Tente novamente.";
}

Exemplos

Exemplo 1: Transferência bancária

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

    $pdo->exec("UPDATE contas SET saldo = saldo - 500 WHERE id = 1");
    $pdo->exec("UPDATE contas SET saldo = saldo + 500 WHERE id = 2");

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

Exemplo 2: Inserção de pedido e itens

try {
    $pdo->beginTransaction();

    $stmt = $pdo->prepare("INSERT INTO pedidos (cliente_id, data) VALUES (?, NOW())");
    $stmt->execute([1]);
    $pedidoId = $pdo->lastInsertId();

    $stmt = $pdo->prepare("INSERT INTO itens_pedido (pedido_id, produto, quantidade) VALUES (?, ?, ?)");
    $stmt->execute([$pedidoId, 'Teclado', 1]);
    $stmt->execute([$pedidoId, 'Mouse', 2]);

    $pdo->commit();
    echo "Pedido criado com sucesso. ID: $pedidoId";
} catch (Exception $e) {
    $pdo->rollback();
    echo "Erro ao criar pedido: " . $e->getMessage();
}

Boas Práticas

  • Sempre configure o modo de erro para exceções (PDO::ERRMODE_EXCEPTION) ao usar transações.
  • Evite transações muito longas; mantenha apenas o necessário dentro do bloco transacional.
  • Verifique se o banco de dados suporta transações e use o motor adequado (ex.: InnoDB no MySQL).
  • Em sistemas críticos, considere o uso de savepoints para controle mais granular.
  • Após o commit ou rollback, a transação é encerrada automaticamente; não é necessário chamar beginTransaction() novamente para uma nova transação.

Referências

Exercícios

  1. Escreva um script PHP que realiza uma transferência bancária entre duas contas, utilizando transações. Inclua tratamento de erros com rollback.
  2. ✓ Resposta:
    try {
        $pdo = new PDO('mysql:host=localhost;dbname=banco', 'root', '');
        $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
        $pdo->beginTransaction();
        $pdo->exec("UPDATE contas SET saldo = saldo - 200 WHERE id = 1");
        $pdo->exec("UPDATE contas SET saldo = saldo + 200 WHERE id = 2");
        $pdo->commit();
        echo "Transferência realizada.";
    } catch (Exception $e) {
        $pdo->rollback();
        echo "Erro: " . $e->getMessage();
    }
    
  3. Crie um código que insira um novo usuário e seu perfil em duas tabelas diferentes (usuarios e perfis), garantindo que ambas as inserções sejam atômicas.
  4. ✓ Resposta:
    try {
        $pdo->beginTransaction();
        $stmt = $pdo->prepare("INSERT INTO usuarios (nome, email) VALUES (?, ?)");
        $stmt->execute(['Maria', 'maria@email.com']);
        $userId = $pdo->lastInsertId();
        $stmt = $pdo->prepare("INSERT INTO perfis (usuario_id, bio) VALUES (?, ?)");
        $stmt->execute([$userId, 'Bio da Maria']);
        $pdo->commit();
        echo "Usuário e perfil criados.";
    } catch (Exception $e) {
        $pdo->rollback();
        echo "Erro: " . $e->getMessage();
    }
    
  5. Explique por que é importante configurar PDO::ATTR_ERRMODE para PDO::ERRMODE_EXCEPTION ao trabalhar com transações.
  6. ✓ Resposta: Configurar o modo de erro para exceções permite que qualquer falha em uma operação SQL lance uma exceção, que pode ser capturada no bloco catch para realizar o rollback. Sem isso, erros silenciosos poderiam passar despercebidos e a transação não seria desfeita, comprometendo a integridade dos dados.
  7. Escreva um exemplo que utiliza savepoints para permitir rollback parcial dentro de uma transação.
  8. ✓ Resposta:
    try {
        $pdo->beginTransaction();
        $pdo->exec("INSERT INTO logs (mensagem) VALUES ('Início')");
        $pdo->exec("SAVEPOINT sp1");
        $pdo->exec("INSERT INTO logs (mensagem) VALUES ('Ponto 1')");
        // Se algo der errado, pode reverter para sp1
        $pdo->exec("ROLLBACK TO SAVEPOINT sp1");
        $pdo->exec("INSERT INTO logs (mensagem) VALUES ('Após rollback')");
        $pdo->commit();
    } catch (Exception $e) {
        $pdo->rollback();
    }
    
  9. Crie um código que verifica se uma transação está ativa antes de tentar fazer commit ou rollback.
  10. ✓ Resposta:
    if ($pdo->inTransaction()) {
        $pdo->commit();
    } else {
        echo "Nenhuma transação ativa.";
    }