DEV Community

Cover image for Dominando Transações de Banco de Dados: Do Conceito à Prática com Laravel
André Moreira
André Moreira

Posted on

Dominando Transações de Banco de Dados: Do Conceito à Prática com Laravel

Dominando Transações de Banco de Dados: Do Conceito à Prática com Laravel

Quantas vezes você se perguntou: "E se der erro na metade da operação?" Transações de banco de dados são o escudo invisível que protege a integridade dos seus dados. Vamos entender isso a fundo.

O Problema Real

Imagine um sistema de transferência bancária:

  • Débito da conta A: ✓
  • Erro de rede
  • Crédito na conta B: ✗

Resultado? Dinheiro desaparece. Transações resolvem isso garantindo "tudo ou nada".

Conceitos Fundamentais: ACID

Antes de codar, precisamos entender os pilares:

Atomicidade (A)

A transação é uma unidade indivisível. Ou todas as operações completam, ou nenhuma. Sem meio termo.

Consistência (C)

O banco sempre sai de um estado válido para outro válido. Regras de negócio são respeitadas.

Isolamento (I)

Transações simultâneas não interferem umas nas outras. Cada uma vê um snapshot consistente dos dados.

Durabilidade (D)

Uma vez confirmada, os dados persistem. Mesmo com falhas de hardware.

Níveis de Isolamento: O Trade-off

Aqui está o segredo que muitos desenvolvedores não dominam completamente:

-- De menor para maior isolamento (maior custo de performance)

READ UNCOMMITTED    -- Dirty reads possíveis ⚠️
READ COMMITTED      -- Padrão do PostgreSQL (equilibrado)
REPEATABLE READ     -- Padrão do MySQL InnoDB
SERIALIZABLE        -- Total segurança, performance limitada
Enter fullscreen mode Exit fullscreen mode

Qual usar? Depende do seu caso de uso:

  • E-commerce: READ COMMITTED geralmente basta
  • Relatórios financeiros: SERIALIZABLE
  • Blog: READ UNCOMMITTED é aceitável

Na Prática com Laravel

1. Transação Básica

use Illuminate\Support\Facades\DB;

DB::transaction(function () {
    $user = User::create([
        'name' => 'João',
        'email' => 'joao@example.com'
    ]);

    Account::create([
        'user_id' => $user->id,
        'balance' => 1000
    ]);
    // Se algo der erro aqui, ambas operações são revertidas
});
Enter fullscreen mode Exit fullscreen mode

2. Controle Manual com Savepoints

DB::beginTransaction();

try {
    // Primeira operação
    $transfer = Transfer::create([
        'from_account' => 1,
        'to_account' => 2,
        'amount' => 100
    ]);

    DB::savepoint(1); // Ponto de restauração

    // Operação arriscada
    $notification = $this->sendNotification($transfer);

    DB::commit();
} catch (Exception $e) {
    DB::rollbackToSavepoint(1); // Volta ao ponto, mas mantém a transação

    // Agora você pode decidir: fazer retry, log, etc
    Log::error('Notification failed', ['transfer_id' => $transfer->id]);

    DB::commit(); // Commita a transferência, ignora notificação
}
Enter fullscreen mode Exit fullscreen mode

3. Definindo Nível de Isolamento

// PostgreSQL
DB::connection()->statement("SET TRANSACTION ISOLATION LEVEL SERIALIZABLE");

DB::transaction(function () {
    // suas operações
});
Enter fullscreen mode Exit fullscreen mode
// MySQL
DB::beginTransaction();
DB::connection()->statement("SET TRANSACTION ISOLATION LEVEL REPEATABLE READ");

// suas operações

DB::commit();
Enter fullscreen mode Exit fullscreen mode

4. Timeout de Transação

// Evite deadlocks com timeout
DB::connection()->statement("SET lock_wait_timeout = 5"); // MySQL: 5 segundos

DB::transaction(function () {
    // Se travar por mais de 5s, lança exception
    AccountLock::create(['account_id' => 1]); // Simula lock
}, maxAttempts: 3); // Tenta 3 vezes antes de desistir
Enter fullscreen mode Exit fullscreen mode

Armadilhas Comuns (e como evitá-las)

❌ Armadilha 1: Transações Muito Longas

// NÃO FAÇA ISSO
DB::transaction(function () {
    // Aqui você está lockando recursos
    $data = fetchFromExternalAPI(); // Pode demorar 30s!

    // Enquanto isso, outras transações ficam esperando
    User::create($data);
});
Enter fullscreen mode Exit fullscreen mode

Solução:

// Faça a operação lenta fora da transação
$data = fetchFromExternalAPI();

DB::transaction(function () use ($data) {
    User::create($data); // Rápido
});
Enter fullscreen mode Exit fullscreen mode

❌ Armadilha 2: Ignorar Exceções

// NÃO FAÇA ISSO
try {
    DB::transaction(function () {
        Order::create(['total' => -100]); // Erro!
    });
} catch (Exception $e) {
    // "Vou ignorar e seguir em frente"
}

// Seu sistema continua inconsistente
Enter fullscreen mode Exit fullscreen mode

❌ Armadilha 3: Transações Aninhadas Sem Cuidado

// Cada DB::transaction() é um novo nível
DB::transaction(function () { // Transação 1
    Order::create([...]);

    DB::transaction(function () { // Não é realmente uma transação aninhada!
        Payment::create([...]);
    });
});
Enter fullscreen mode Exit fullscreen mode

Use savepoints em vez disso:

DB::beginTransaction();
try {
    Order::create([...]);
    DB::savepoint(1);

    try {
        Payment::create([...]);
    } catch (Exception $e) {
        DB::rollbackToSavepoint(1);
        // Tenta algo diferente
    }

    DB::commit();
}
Enter fullscreen mode Exit fullscreen mode

Monitoramento em Produção

Você está monitorando suas transações? Deveria estar:

// Listener customizado
Event::listen('illuminate.query', function ($query) {
    if ($query->getDurationInSeconds() > 5) {
        Log::warning('Slow transaction detected', [
            'sql' => $query->sql,
            'bindings' => $query->bindings,
            'time' => $query->getDurationInSeconds()
        ]);
    }
});
Enter fullscreen mode Exit fullscreen mode

Ou use ferramentas como:

  • Datadog APM: monitora transações em produção
  • New Relic: identifica deadlocks
  • PostgreSQL logs: log_min_duration_statement = 1000 (queries > 1s)

Testando Transações

// Use RefreshDatabase do Laravel
class TransactionTest extends TestCase
{
    use RefreshDatabase;

    public function test_transaction_rollback()
    {
        $this->assertDatabaseCount('users', 0);

        try {
            DB::transaction(function () {
                User::create(['name' => 'João']);
                throw new Exception('Erro!');
            });
        } catch (Exception $e) {}

        $this->assertDatabaseCount('users', 0); // Rollback funcionou!
    }
}
Enter fullscreen mode Exit fullscreen mode

Checklist Final: Você Está Fazendo Certo?

  • Transações apenas para operações que precisam de atomicidade
  • Operações lentas fora da transação
  • Nível de isolamento apropriado para o caso de uso
  • Tratamento de exceções adequado
  • Logs de transações falhadas
  • Monitoramento em produção
  • Testes de rollback
  • Timeout configurado

Próximos Passos

Transações são fundamentais, mas há mais:

  • Distributed Transactions: quando você tem múltiplos bancos
  • Event Sourcing: alternativa para auditoria
  • CQRS Pattern: separação leitura/escrita

Conclusão

Transações não são apenas um recurso técnico—são o fundamento da confiabilidade. Dominar seus conceitos, níveis de isolamento e armadilhas é o que separa código que "funciona" de código que é robusto.

Qual é sua maior dificuldade com transações? Compartilha nos comentários!


Quer mergulhar mais fundo?

Top comments (0)