Transações com PDO

1. Introdução às Transações no PDO

Transações são unidades lógicas de trabalho que garantem que um conjunto de operações no banco de dados seja executado de forma atômica. Isso significa que todas as operações dentro da transação devem ser concluídas com sucesso ou, em caso de falha, nenhuma delas deve ter efeito permanente. Esse princípio segue o acrônimo ACID: Atomicidade, Consistência, Isolamento e Durabilidade.

No contexto do PDO (PHP Data Objects), transações são essenciais para operações que envolvem múltiplas instruções SQL que precisam ser tratadas como uma única unidade. Exemplos clássicos incluem transferências bancárias, processamento de pedidos, ou qualquer cenário onde a integridade dos dados depende da execução completa de um conjunto de operações.

Para utilizar transações com PDO, seu ambiente PHP precisa ter a extensão PDO habilitada e o banco de dados (MySQL, PostgreSQL, SQLite, etc.) deve suportar transações. Felizmente, todos os principais SGBDs oferecem esse suporte.

2. Iniciando e Finalizando Transações

O PDO fornece três métodos fundamentais para gerenciar transações:

  • PDO::beginTransaction() — desativa o modo autocommit e inicia uma transação
  • PDO::commit() — confirma todas as operações realizadas desde o início da transação
  • PDO::rollBack() — reverte todas as operações realizadas desde o início da transação

Vamos ver um exemplo prático de transferência bancária entre duas contas:

<?php

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

    // Inicia a transação
    $pdo->beginTransaction();

    // Deduz 500 da conta de origem
    $stmt = $pdo->prepare("UPDATE contas SET saldo = saldo - ? WHERE id = ?");
    $stmt->execute([500, 1]);

    // Adiciona 500 à conta de destino
    $stmt = $pdo->prepare("UPDATE contas SET saldo = saldo + ? WHERE id = ?");
    $stmt->execute([500, 2]);

    // Confirma as alterações
    $pdo->commit();

    echo "Transferência realizada com sucesso!";

} catch (PDOException $e) {
    // Em caso de erro, reverte tudo
    $pdo->rollBack();
    echo "Erro na transferência: " . $e->getMessage();
}

3. Tratamento de Erros e Exceções em Transações

O uso de try-catch é a abordagem recomendada para gerenciar transações, pois garante que qualquer exceção seja capturada e a transação seja revertida adequadamente.

<?php

function transferir($pdo, $origem, $destino, $valor) {
    try {
        $pdo->beginTransaction();

        // Verifica saldo da conta de origem
        $stmt = $pdo->prepare("SELECT saldo FROM contas WHERE id = ? FOR UPDATE");
        $stmt->execute([$origem]);
        $saldoOrigem = $stmt->fetchColumn();

        if ($saldoOrigem < $valor) {
            throw new Exception("Saldo insuficiente na conta de origem");
        }

        $pdo->prepare("UPDATE contas SET saldo = saldo - ? WHERE id = ?")
           ->execute([$valor, $origem]);
        $pdo->prepare("UPDATE contas SET saldo = saldo + ? WHERE id = ?")
           ->execute([$valor, $destino]);

        $pdo->commit();
        return true;

    } catch (Exception $e) {
        if ($pdo->inTransaction()) {
            $pdo->rollBack();
        }
        throw $e; // Relança a exceção para tratamento externo
    }
}

Note o uso de PDO::inTransaction() para verificar se a transação ainda está ativa antes de chamar rollBack(). Isso evita erros caso a transação já tenha sido finalizada por algum motivo.

4. Transações Aninhadas e Comportamento do SGBD

O PDO não suporta transações aninhadas nativamente. Chamar beginTransaction() enquanto uma transação já está ativa lançará uma exceção. No entanto, é possível simular savepoints usando comandos SQL diretamente:

<?php

$pdo->beginTransaction();

// Cria um savepoint
$pdo->exec("SAVEPOINT ponto1");

// Operações parciais
$pdo->exec("UPDATE produtos SET estoque = estoque - 1 WHERE id = 10");

// Se algo der errado, pode reverter até o savepoint
$pdo->exec("ROLLBACK TO SAVEPOINT ponto1");

// Ou confirmar tudo
$pdo->commit();

É importante conhecer as diferenças entre SGBDs:
- MySQL (InnoDB): suporta transações e savepoints
- PostgreSQL: suporte completo a transações e savepoints
- SQLite: suporta transações, mas com limitações em concorrência
- MyISAM (MySQL): não suporta transações

5. Isolamento de Transações e Bloqueios

Os níveis de isolamento definem como as transações interagem entre si. O PDO permite configurar o nível via PDO::exec():

<?php

// Configura nível de isolamento para evitar leitura suja
$pdo->exec("SET TRANSACTION ISOLATION LEVEL READ COMMITTED");
$pdo->beginTransaction();

// Consulta que não verá dados não confirmados de outras transações
$stmt = $pdo->query("SELECT saldo FROM contas WHERE id = 1");

Os níveis disponíveis são:
- READ UNCOMMITTED: menor isolamento, permite leitura suja
- READ COMMITTED: evita leitura suja (padrão no PostgreSQL)
- REPEATABLE READ: garante leituras consistentes (padrão no MySQL InnoDB)
- SERIALIZABLE: maior isolamento, executa transações como se fossem sequenciais

6. Práticas Avançadas com Transações

Combinação de transações com prepared statements e operações em lote:

<?php

$pdo->beginTransaction();

try {
    $stmt = $pdo->prepare("INSERT INTO logs (usuario_id, acao, data) VALUES (?, ?, NOW())");

    // Inserção em massa dentro da transação
    $dados = [
        [1, 'login'],
        [2, 'logout'],
        [3, 'atualizacao'],
    ];

    foreach ($dados as $linha) {
        $stmt->execute($linha);
    }

    // lastInsertId() funciona dentro da transação
    $ultimoId = $pdo->lastInsertId();

    $pdo->commit();

} catch (Exception $e) {
    $pdo->rollBack();
    echo "Erro no processamento em lote: " . $e->getMessage();
}

7. Boas Práticas e Erros Comuns

Boas práticas:
- Sempre verifique se a transação está ativa antes de commit() ou rollBack()
- Mantenha transações curtas para evitar bloqueios prolongados
- Use try-catch-finally para garantir o fechamento da transação
- Configure PDO::ATTR_ERRMODE para PDO::ERRMODE_EXCEPTION

Erros comuns:

// ERRADO: esquecer de tratar exceções
$pdo->beginTransaction();
$pdo->query("UPDATE contas SET saldo = saldo - 100 WHERE id = 1");
// Se ocorrer erro aqui, a transação fica aberta!
$pdo->commit();

// CORRETO
try {
    $pdo->beginTransaction();
    $pdo->query("UPDATE contas SET saldo = saldo - 100 WHERE id = 1");
    $pdo->commit();
} catch (Exception $e) {
    $pdo->rollBack();
}

8. Conclusão e Comparação com Outras Abordagens

Transações com PDO oferecem controle fino e direto sobre operações de banco de dados em PHP. Embora ORMs como Doctrine (com EntityManager::flush()) abstraiam esse gerenciamento, o PDO puro é mais leve e adequado para aplicações que exigem performance e controle granular.

Quando usar PDO puro:
- Aplicações simples ou legadas
- Operações que exigem controle preciso de transações
- Performance crítica

Quando optar por abstrações:
- Projetos complexos com múltiplas entidades
- Equipes que preferem abstração de banco de dados
- Necessidade de portabilidade entre SGBDs

Dominar transações com PDO é fundamental para qualquer desenvolvedor PHP que trabalhe com bancos de dados relacionais, garantindo integridade e confiabilidade nas operações.

Referências