Soft Delete no Laravel

Frequentemente, deletamos dados que não são mais necessários, mas, nesse processo, é provável que você já tenha excluído acidentalmente algo importante. Quando se trata de arquivos, os sistemas operacionais geralmente possuem uma lixeira para recuperação, mas em sistemas não é tão comum termos esse tipo de recurso. É ai que o conceito de Soft Delete entra em cena.

O soft delete é uma técnica utilizada em sistemas de gerenciamento de dados para marcar registros como deletados sem realmente removê-los do banco de dados. Em vez de excluir permanentemente os dados, um indicador, como um campo booleano ou uma data de exclusão, é atualizado para sinalizar que o registro está inativo. Isso permite que os dados sejam facilmente restaurados, se necessário, e facilita auditorias e histórico de alterações.

O Laravel oferece recursos que permitem trabalhar com soft delete de forma muito simples, os quais vamos explorá-los a seguir.

Para começar vamos criar o migrate do nosso projeto, neste exemplo iremos gerenciar o cadastro de Tarefas de um projeto.

public function up()
{
    Schema::create('projetos', function (Blueprint $table) {
        $table->id();
        $table->string('slug')->unique();            
        $table->string('titulo');
        $table->text('descricao'); 
        $table->timestamps();
        $table->softDeletes(); // adiciona coluna "deleted_at"
   });

   Schema::create('tarefas', function (Blueprint $table) {
        $table->id();
        $table->string('slug')->unique();            
        $table->string('titulo');
        $table->text('descricao'); 
        $table->dateTime('inicio_em'); 
        $table->dateTime('fim_em')->nullable(); 
        $table->timestamps();
        $table->softDeletes(); // adiciona coluna "deleted_at"
   });
}

Como podemos ver, através do método softDeletes adicionamos uma coluna nas tabelas onde queremos aplicar o recurso de soft delete. Esse método adiciona uma coluna deleted_at do tipo datetime, que será usada para indicar se um registro foi excluído ou não. Quando um registro é deletado, a data de exclusão é registrada nessa coluna, marcando-o assim como excluído.

O próximo passo é a inclusão do trait SoftDeletes nas classes Model correspondentes.

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;

class Projeto extends Model
{
    use SoftDeletes; // Adicione esta linha para habilitar o soft delete

    // ....
}

class Tarefa extends Model
{
    use SoftDeletes; // Adicione esta linha para habilitar o soft delete

    // ....
}

Pronto é isso, muito obrigado. Até mais ..

Brincadeira. Agora, ao usar o método delete de um Model, este não apagará o registro no banco de dados, apenas atualizará a coluna deleted_at com a data atual.

    public function destroy($id)
    {
        $projeto = Projeto::find($id);
        $projeto->delete();
        
        // .... resto da lógica
    }

A partir deste momento, quando utilizarmos os métodos de recuperação(select) como find, all, get, etc. eles não mais irá retornar este registro que possui um valor na coluna deleted_at.

App\Models\Projeto::create([
	'titulo' => 'Projeto 1',
	'slug' => 'projeto-1',
	'descricao' => 'Descrição projeto 1',
]);
App\Models\Projeto::create([
	'titulo' => 'Projeto 2',
	'slug' => 'projeto-2',
	'descricao' => 'Descrição projeto 2',
]);
App\Models\Projeto::create([
	'titulo' => 'Projeto 3',
	'slug' => 'projeto-3',
	'descricao' => 'Descrição projeto 3',
]);

$proj = App\Models\Projeto::find(1);
$proj->delete();

$projetos = App\Models\Projeto::select('id', 'titulo')->get()->toArray();
//[
//    [
//      "id" => 2,
//      "titulo" => "Projeto 2",
//    ],
//    [
//      "id" => 3,
//      "titulo" => "Projeto 3",
//    ],
//  ]

Como podemos ver, um registro deletado com o método delete não é retornado ao usar o método get. No entanto, se verificarmos diretamente no banco de dados com um SELECT, veremos que o registro continua presente, apenas com a coluna deleted_at preenchida com um valor.

mysql> select * from projetos;
+----+-----------+-----------+---------------------+---------------------+---------------------+---------------------+
| id | slug      | titulo    | descricao           | created_at          | updated_at          | deleted_at          |
+----+-----------+-----------+---------------------+---------------------+---------------------+---------------------+
|  1 | projeto-1 | Projeto 1 | Descrição projeto 1 | 2024-05-19 15:10:16 | 2024-05-19 15:10:42 | 2024-05-19 15:10:42 |
|  2 | projeto-2 | Projeto 2 | Descrição projeto 2 | 2024-05-19 15:10:16 | 2024-05-19 15:10:16 | NULL                |
|  3 | projeto-3 | Projeto 3 | Descrição projeto 3 | 2024-05-19 15:10:17 | 2024-05-19 15:10:17 | NULL                |
+----+-----------+-----------+---------------------+---------------------+---------------------+---------------------+

Para recuperarmos os registros que foram deletados, devemos encadear uma chamada ao método withTrashed(), que incluirá os registros “soft deletados” na consulta.

App\Models\Projeto::withTrashed()->select('id', 'titulo')->get()->toArray();
//[
//    [
//      "id" => 1,
//      "titulo" => "Projeto 1",
//    ],
//    [
//      "id" => 2,
//      "titulo" => "Projeto 2",
//    ],
//    [
//      "id" => 3,
//      "titulo" => "Projeto 3",
//    ],
//  ]

Para recuperar exclusivamente os registros deletados, utilizamos o método onlyTrashed(). Este método permite filtrar a consulta, retornando apenas os registros que possuem a coluna deleted_at preenchida, indicando que foram marcados como excluídos através do soft delete.

App\Models\Projeto::onlyTrashed()->select('id', 'titulo')->get()->toArray();
// [
//     [
//       "id" => 1,
//       "titulo" => "Projeto 1",
//     ],
//   ]

Se quisermos deletar um registro de forma definitiva, utilizamos o método forceDelete(), que removerá o registro permanentemente do banco de dados, impossibilitando sua recuperação.

$projeto = Projeto::withTrashed()->find($id);
$projeto->forceDelete();

Para recuperar um registro deletado com soft delete, utilizamos o método restore(), que redefine o valor da coluna deleted_at para null. Assim, o registro volta a ser recuperado normalmente pelos métodos get, all, etc., sem a necessidade de usar os métodos *Trashed().

$projeto = Projeto::withTrashed()->find($id);
$projeto->restore();

Deletando em cascata com Soft Delete

Como você pode imaginar, usar onDelete('cascade') na nossa migração não funcionará como desejado, pois a exclusão só ocorrerá quando o registro for removido fisicamente do banco. Com o soft delete, estamos realizando apenas uma remoção lógica.

Para realizar um cascade com soft delete, devemos utilizar um model observer do Laravel. O observer dispara um código quando um evento ocorre no model, como deletar e restaurar um registro. Para adicionar um observer ao model, criamos o método boot() na classe de model e invocamos o método associado ao evento que queremos observar, passando uma função para executar quando esse evento ocorrer. A função passada recebe como argumento o objeto de model que esta “sofrendo” o evento.

No nosso exemplo, queremos que, ao deletar um Projeto, as tarefas associadas a ele também sejam deletadas. Da mesma forma, quando o Projeto for restaurado, queremos que as tarefas deletadas sejam restauradas com ele.

class Projeto extends Model
{
    // ...

    public static function boot ()
    {
            parent::boot();

            self::deleting(function ($projeto) {
                foreach ($projeto->tarefas as $tarefa){
                    $tarefa->delete();
                }
            });
            self::restoring(function ($projeto) {
                foreach ($projeto->tarefas()->onlyTrashed()->get() as $tarefa){
                    $tarefa->restore();
                 }
            }
        });
    }

}

Como podemos ver, adicionamos o método boot() e, dentro dele, chamamos o método deleting para adicionar um observador ao evento de deleção do model. Quando esse evento ocorre, ele itera sobre as tarefas associadas ao projeto e também as deleta. No método restoring, adicionamos um observador que é chamado quando um evento de restauração ocorre. Nesse caso, ele itera sobre as tarefas “trashed” do projeto e executa o restore em cada uma delas para recuperá-las junto com o projeto.

Bom, isso é o básico sobre soft delete. É importante considerar que nem toda tabela precisa desse recurso. Reserve-o para situações onde a perda de dados seja muito problemática ou quando a retenção de dados seja uma política essencial do negócio.

Segue um exemplo de cadastro simples utilizando soft delete