DEV Community

Denis Augusto
Denis Augusto

Posted on

Soft Deletes no Laravel: apague sem apagar de verdade

"Some do banco" nem sempre é o que você quer

Imagina a cena: o cliente liga desesperado porque um funcionário apagou um pedido importante. Você abre o banco, e... não tem. Sumiu. DELETE é DELETE. Agora é backup ou choro.

Agora imagina a mesma cena, só que você digita $pedido->restore() e o registro volta como se nada tivesse acontecido. Herói do dia.

Essa é a proposta do Soft Delete: em vez de apagar a linha de verdade, o Laravel marca ela como apagada e finge que ela não existe mais nas consultas. Ela continua lá, só invisível. E dá pra trazer de volta quando quiser.

O problema: delete() é definitivo

Sem soft delete, isso aqui é irreversível:

$pedido->delete(); // tchau. pra sempre. 💀
Enter fullscreen mode Exit fullscreen mode

Some do banco, some dos relatórios, some da vida. Se alguém apagou por engano, ou se você precisa de histórico pra auditoria, azar. E é impressionante como "apagou por engano" acontece com frequência em produção.

A solução: três passos e pronto

Primeiro, adiciona a coluna deleted_at na migration. O Laravel tem um helper só pra isso:

Schema::table('pedidos', function (Blueprint $table) {
    $table->softDeletes(); // cria a coluna deleted_at (nullable)
});
Enter fullscreen mode Exit fullscreen mode

Depois, usa a trait SoftDeletes na model:

use Illuminate\Database\Eloquent\SoftDeletes;

class Pedido extends Model
{
    use SoftDeletes;
}
Enter fullscreen mode Exit fullscreen mode

Pronto. Agora o delete() mudou de comportamento sem você tocar em mais nada:

$pedido->delete();
// não apaga a linha — só preenche deleted_at com a data/hora de agora
Enter fullscreen mode Exit fullscreen mode

E o melhor: as consultas normais ignoram automaticamente os registros apagados. Pedido::all() não traz nada que foi soft-deletado. Você não precisa filtrar nada na mão.

Trazendo de volta e vendo os apagados

Restaurar é uma linha:

$pedido->restore(); // deleted_at volta pra null, registro "revive"
Enter fullscreen mode Exit fullscreen mode

Pra incluir os apagados numa consulta, withTrashed:

Pedido::withTrashed()->get(); // traz tudo, apagados junto
Enter fullscreen mode Exit fullscreen mode

Pra ver os apagados (útil pra uma tela de "lixeira"):

Pedido::onlyTrashed()->get();
Enter fullscreen mode Exit fullscreen mode

E se precisar apagar de verdade, aí sim, sem volta:

$pedido->forceDelete(); // agora foi pro além de verdade
Enter fullscreen mode Exit fullscreen mode

Como usar na prática

Lixeira pro usuário. Uma tela onde ele apaga itens, mas tem 30 dias pra restaurar. onlyTrashed() lista, restore() recupera.

Auditoria e histórico. Setores como financeiro e jurídico costumam exigir que nada suma de verdade. Soft delete mantém o rastro sem poluir as telas do dia a dia.

Checar o estado no código. Dá pra saber se um registro tá apagado:

if ($pedido->trashed()) {
    // esse aqui tá na lixeira
}
Enter fullscreen mode Exit fullscreen mode

Pegadinha 1: unique não sabe de soft delete

Essa pega muita gente. Você tem uma coluna email com índice unique. O usuário joao@email.com é soft-deletado. Ele tenta se cadastrar de novo... e o banco recusa, porque a linha antiga ainda está lá, ocupando o email.

O unique do banco não liga pro deleted_at — pra ele, a linha existe. Na validação, você precisa dizer isso explicitamente. Na Rule unique, ignore os apagados:

use Illuminate\Validation\Rule;

Rule::unique('usuarios', 'email')->whereNull('deleted_at');
Enter fullscreen mode Exit fullscreen mode

Assim a validação só considera os registros ativos, e o email "liberado" pelo soft delete volta a funcionar.

Pegadinha 2: relacionamento com registro apagado

Outra que confunde. O pedido foi soft-deletado, mas você carrega ele por um relacionamento e o item some:

$cliente->pedidos; // não traz os pedidos apagados
Enter fullscreen mode Exit fullscreen mode

Isso normalmente é o que você quer. Mas quando não for — tipo montar um histórico completo do cliente — o relacionamento também aceita withTrashed:

public function pedidos()
{
    return $this->hasMany(Pedido::class)->withTrashed();
}
Enter fullscreen mode Exit fullscreen mode

Ou pontualmente, sem mexer na model:

$cliente->pedidos()->withTrashed()->get();
Enter fullscreen mode Exit fullscreen mode

Bônus: e agora?

Olha as tabelas do seu projeto e pergunta pra cada uma: "se alguém apagar isso por engano, dói?" Pedido, fatura, contrato, usuário — provavelmente sim. Categoria de teste que você cria e joga fora — provavelmente não. Soft delete é ótimo, mas não precisa estar em tudo; coluna a mais e query um pouco mais complexa têm um custo.

E cuidado com a ideia de "guardar tudo pra sempre". Dependendo do dado (LGPD, por exemplo), você pode ser obrigado a apagar de verdade depois de um tempo. Aí um job periódico com forceDelete() nos registros antigos resolve.

Antes de você fechar a aba

Já perdeu algum registro importante em produção por causa de um delete() sem volta? Ou já foi salvo por um soft delete que alguém teve a sabedoria de colocar antes?

Me conta aí embaixo. E se esse post pode evitar um choro futuro no seu projeto, salva ele e manda pro colega que ainda apaga tudo direto no banco. 😅

Top comments (0)