Transações com PDO
Esta aula aborda o uso de transações com PDO em PHP, explicando como agrupar operações SQL em blocos atômicos usando beginTransaction, commit e rollback. Você aprenderá quando utilizar transações, como tratar erros adequadamente e verá exemplos práticos de implementação.
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()ourollback()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
- PHP: PDO::beginTransaction - Manual
- PHP: PDO::commit - Manual
- PHP: PDO::rollback - Manual
- PHP: PDO::inTransaction - Manual
- MySQL: COMMIT e ROLLBACK
- PostgreSQL: Transações
Exercícios
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(); }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(); }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(); }Explique o que acontece se você chamar
commit()duas vezes seguidas sem uma nova transação.✓ Resposta:Chamarcommit()quando não há transação ativa lançará uma exceçãoPDOExceptioncom a mensagem "There is no active transaction". Portanto, é importante verificar cominTransaction()ou garantir que o fluxo sempre tenha uma transação aberta.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"; }