DEV Community

Denis Augusto
Denis Augusto

Posted on

DB::transaction(): o try/catch que salva seu banco de dados

Salvou o pedido, mas não salvou os itens. E agora?

Imagina a cena: seu checkout cria o pedido, dá baixa no estoque e registra o pagamento. Três operações no banco, uma atrás da outra.

Aí no meio do caminho o banco cai. Ou uma validação estoura. Ou a API de pagamento dá timeout.

Resultado? O pedido foi criado, o estoque foi debitado, mas o pagamento nunca registrou. Você acabou de vender um produto de graça. 🎉

Esse é o pesadelo do dado inconsistente. E a solução tem nome: transaction.

O código que parece inofensivo (mas não é)

Olha esse controller bem comum:

public function store(Request $request)
{
    $pedido = Pedido::create($request->only('cliente_id', 'total'));

    foreach ($request->itens as $item) {
        $pedido->itens()->create($item);
        Produto::find($item['produto_id'])->decrement('estoque', $item['qtd']);
    }

    Pagamento::create([
        'pedido_id' => $pedido->id,
        'valor' => $pedido->total,
    ]);

    return response()->json($pedido);
}
Enter fullscreen mode Exit fullscreen mode

Parece limpo, né? O problema é que cada linha grava no banco na hora.

Se der ruim na criação do pagamento, o pedido e os itens já estão salvos. Não tem volta. Seu banco fica com um Frankenstein: metade de uma operação que era pra ser uma coisa só.

O conceito por trás: atomicidade

Uma transaction resolve isso com uma ideia simples: tudo ou nada.

Ou todas as operações dão certo e são gravadas de vez (o commit), ou qualquer uma falha e o banco desfaz tudo como se nada tivesse acontecido (o rollback).

É o mesmo princípio de uma transferência bancária: não existe "saiu da minha conta mas não entrou na sua". Ou acontece por inteiro, ou não acontece.

A solução: DB::transaction()

O Laravel deixa isso ridiculamente fácil. Você embrulha o código num DB::transaction() e pronto:

use Illuminate\Support\Facades\DB;

public function store(Request $request)
{
    $pedido = DB::transaction(function () use ($request) {
        $pedido = Pedido::create($request->only('cliente_id', 'total'));

        foreach ($request->itens as $item) {
            $pedido->itens()->create($item);
            Produto::find($item['produto_id'])->decrement('estoque', $item['qtd']);
        }

        Pagamento::create([
            'pedido_id' => $pedido->id,
            'valor' => $pedido->total,
        ]);

        return $pedido;
    });

    return response()->json($pedido);
}
Enter fullscreen mode Exit fullscreen mode

Só isso. Se qualquer linha dentro da closure lançar uma exception, o Laravel faz o rollback automático de tudo. Nada é gravado.

Se rodar até o fim sem erro, ele dá o commit. E de bônus: o que a closure retornar (return $pedido) vira o retorno do DB::transaction(). Prático demais.

Repara que não precisa de try/catch, nem de commit, nem de rollBack na mão. A closure faz o trabalho sujo pra você.

Como usar na prática

No checkout que a gente viu: garante que pedido, itens, estoque e pagamento vivem e morrem juntos. Nunca mais um pedido órfão.

Num cadastro com múltiplas tabelas: criar o usuário, o perfil e as configurações padrão. Se o perfil falhar, o usuário não fica solto no banco sem perfil.

Numa importação em lote: processar 500 linhas de uma planilha. Se a linha 480 tiver um erro, você não quer 479 registros pela metade. Ou entra tudo, ou você corrige a planilha e roda de novo.

A versão manual (pra quando você precisa de controle)

Às vezes a closure não dá conta — você precisa decidir quando fazer commit ou rollback com base em alguma lógica. Aí usa a forma manual:

DB::beginTransaction();

try {
    $pedido = Pedido::create($dados);
    // ... mais operações ...

    DB::commit();
} catch (\Throwable $e) {
    DB::rollBack();

    report($e); // loga o erro pra você investigar depois
    throw $e;   // relança pra não engolir o problema em silêncio
}
Enter fullscreen mode Exit fullscreen mode

Funciona igual, só que você está no controle. Mas repara: se você usar essa versão, não engula a exception. Fazer o rollback e seguir a vida como se nada tivesse acontecido esconde o bug. Loga e relança.

Na dúvida, prefira o DB::transaction(). Menos código, menos chance de esquecer um rollBack.

A pegadinha que pega todo mundo

Transaction serve pra banco de dados. Só isso.

O erro clássico é enfiar coisas que não são banco dentro dela, achando que vão ser "desfeitas" também:

DB::transaction(function () use ($pedido) {
    Pedido::create($pedido);

    Mail::to($cliente)->send(new PedidoConfirmado); // ⚠️ perigo
    Storage::put('nota.pdf', $conteudo);            // ⚠️ perigo
});
Enter fullscreen mode Exit fullscreen mode

Se a transaction der rollback depois disso, o pedido some do banco — mas o e-mail já foi enviado e o arquivo já foi salvo. Rollback não volta no tempo pro mundo real.

A regra de ouro: dentro da transaction, só banco. E-mail, envio de arquivo, chamada de API, disparo de job — deixa pra depois do commit.

O Laravel até te ajuda nisso com DB::afterCommit() (e jobs que implementam ShouldQueue já respeitam isso automaticamente se você configurar after_commit). Assim o e-mail só dispara quando o commit deu certo de verdade.

Bônus: e agora?

Se você trabalha com Service Layer, a transaction geralmente mora lá dentro do método do service — não no controller. O controller só orquestra, o service garante a consistência.

E vale abrir a doc pra dar uma olhada em retry (o segundo parâmetro do DB::transaction(), útil pra deadlocks) e em transactions aninhadas com savepoints. Assunto pra um próximo post. 😉

Antes de você fechar a aba

Se você tem algum fluxo que grava em várias tabelas seguidas e ainda não usa transaction, esse é o sinal pra revisar. É o tipo de bug que não aparece nos testes felizes e só dá as caras em produção, numa sexta à noite.

Você já tomou um susto com dado pela metade no banco? Conta aí nos comentários — aposto que tem história boa.

E se esse post te salvou de um checkout quebrado, compartilha com aquele colega que ainda faz três create() seguidos sem rede de proteção. 🙂

Top comments (0)