Soft Delete com 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