Você sabe o que é "softdelete"? Parte 1

marciopolicarpo

Marcio Policarpo

Posted on June 28, 2022

Você sabe o que é "softdelete"? Parte 1

Softdelete é um recurso disponível na grande maioria dos ORMs do mercado.

A ideia por trás deste conceito é não listar registros marcados como deletados quando a consulta é feita através do modelo objeto-relacional.

Na prática o que querybuilder do ORM faz é injetar uma instrução para mostrar somente registros onde a coluna "deleted_at" (especificamente o Eloquent ORM) esteja nula.

Existem aplicações que fazem uso desse recurso quando não querem perder o registro deletado deixando-o invisível.

Para obter o mesmo comportamento usando SQL puro é preciso incluir esse filtro explicitamente na cláusula where.

Entretanto este tipo de implementação (SQL puro) traz riscos à aplicação pois torna necessário codificar a mesma condição em diversos pontos do projeto.

Detalhes acerca da performance não serão abordados neste artigo porque o foco está em mostrar como implementar e testar esta técnica.


Ferramentas necessárias

  • PHP 8 ou superior
  • MySQL Server versão 5.7.38 ou superior
  • Composer versão 2.0 ou superior
  • Editor de textos

Existem instaladores que trazem embutidos o PHP, MySQL e Apache (servidor web), como WAMPSERVER, XAMPP e Laragon.

Deixo a seu critério a escolha de alguns desses pacotes, ou outro que seja familiar a você.

Para o editor de textos estou utilizando o VSCode, por ser gratuito e permitir a instalação de diversas extensões.


Projeto

O projeto a ser desenvolvido é uma aplicação Laravel conhecido framework de desenvolvimento web e está, no momento em que escrevo este artigo, na versão 9.

Repositório

O projeto está publicado no GitHub e pode ser acessado através deste link:
Larave 9 - Soft Deletes


Para criar um novo projeto acesse o terminal de sua preferência e digite o seguinte comando:

composer create-project --prefer-dist laravel/laravel softdelete
Enter fullscreen mode Exit fullscreen mode

O tempo para conclusão desta etapa dependerá de alguns fatores como velocidade de conexão disponível e configuração do computador.

Quando esta etapa estiver concluída, acesse a pasta do projeto porque a partir deste ponto toda interação no terminal deve ser executada dentro da pasta do projeto.

Dito isto, vamos criar a migração.

php artisan make:migration create_invoices_table
Enter fullscreen mode Exit fullscreen mode

Além das colunas desejadas, precisamos adicionar uma coluna para guardar a data e hora que o registro foi "deletado".

O schema builder possui um método helper específico para criação da coluna para atender ao conceito de soft-delete.

$table->softDeletes('deleted_at', 0);
Enter fullscreen mode Exit fullscreen mode

O método $table->softDeletes tem dois parâmetros opcionais. No primeiro informamos o nome da coluna e o no segundo a precisão.

Se chamarmos o método sem informar parâmetros, o schema builder criará uma coluna chamada deleted_at com precisão de segundos.

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('invoices', function (Blueprint $table) {
            $table->increments('id');
            $table->timestamps();
            $table->date('issue_date');
            $table->string('customer_name', 50);
            $table->string('customer_email', 50);
            $table->string('address', 100);
            $table->string('city', 100);
            $table->string('country', 100);
            $table->string('post_code', 16);
            $table->float('tax');
            $table->smallInteger('total_items');
            $table->float('total_value');
            $table->softDeletes();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::table('invoices', function(Blueprint $table) {
            $table->dropSoftDeletes();
        });
        Schema::dropIfExists('invoices');
    }
};

Enter fullscreen mode Exit fullscreen mode

Em seguida, vamos criar e configurar o modelo.

php artisan make:model Invoice
Enter fullscreen mode Exit fullscreen mode

Fazemos isso adicionando o namespace Illuminate\Database\Eloquent\SoftDeletes e referenciando a classe SoftDeletes ao use da classe.

Como boa prática indicamos o nome da tabela para o modelo através da propriedade projegida $table.

Além disso, vou deixar as colunas id, created_ate updated_at invisíveis para consultas feitas através do modelo.

<?php

namespace App\Models;

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

class Invoice extends Model
{
    use HasFactory, SoftDeletes;

    protected $table = 'invoices';

    protected $hidden = [
        'id',
        'created_at',
        'updated_at',
    ];
}

Enter fullscreen mode Exit fullscreen mode

Agora criaremos a controller para integarir com a camada de persistência. Desenvolveremos inicialmente dois métodos.

O primeiro retornará todos os registros do banco.

public function getInvoices()
{
    $invoices = Invoice::all();
    $totalRecords = $invoices->count();

    return response()->json([
        'total' => $totalRecords,
        'data' => $invoices,
    ]);
}
Enter fullscreen mode Exit fullscreen mode

E o segundo retornará o registro de acordo com o id informado no parâmetro.

public function getInvoiceById($id)
{
    $invoice = Invoice::find($id);

    if ($invoice) {
        return $invoice;
    } else {
        return response()->json(['message' => 'Invoice not found'], 404);
    }
}
Enter fullscreen mode Exit fullscreen mode

Note que em ambos estou utilizando o modelo para fazer as consultas.


Em seguida criaremos as rotas para acessar os métodos da controller, editando o arquivo \route\api.php.

<?php

use App\Http\Controllers\InvoiceController;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;

Route::controller(InvoiceController::class)->prefix('/invoice')->group(function () {
    Route::get('/', 'getInvoices');
    Route::get('/{id}/info', 'getInvoiceById');
});

Enter fullscreen mode Exit fullscreen mode

Para ajudar no processo de inserção de registros no banco de dados utilizaremos a técnica de model factory, criando uma classe que herda de \Illuminate\Database\Eloquent\Factories\Factory.

Acessando novamente o terminal digitaremos o seguinte comando:

php artisan make:factory InvoiceFactory
Enter fullscreen mode Exit fullscreen mode

Abrindo a classe recém criada vamos editar o método definition(), adicionando instruções para cada coluna da migração.

<?php

namespace Database\Factories;

use Illuminate\Database\Eloquent\Factories\Factory;

/**
 * @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Invoice>
 */
class InvoiceFactory extends Factory
{
    /**
     * Define the model's default state.
     *
     * @return array<string, mixed>
     */
    public function definition()
    {
        return [
            'issue_date' => $this->faker->dateTimeBetween('-2 years', 'now'),
            'customer_name' => $this->faker->name(),
            'customer_email' => $this->faker->safeEmail(),
            'address' => $this->faker->address(),
            'city' => $this->faker->city(),
            'country' => $this->faker->country(),
            'post_code' => $this->faker->postcode(),
            'tax' => 3,
            'total_items' => $this->faker->numberBetween(1, 36),
            'total_value' => $this->faker->randomFloat(2, 0, 3000),
            //
        ];
    }
}

Enter fullscreen mode Exit fullscreen mode

Para concluir vamos editar o método run() da classe \database\seeders\DatabaseSeeder.php adicionando uma chamada para o helper factory. Este método helper recebe dois parâmetros opcionais onde o primeiro indica quantos registros devem ser criados e o segundo define métodos de manipulação de estado para o modelo.

Especificamente para este projeto criaremos 100 registros.

<?php

namespace Database\Seeders;

use App\Models\Invoice;
use Illuminate\Database\Seeder;
use Illuminate\Database\Console\Seeds\WithoutModelEvents;

class DatabaseSeeder extends Seeder
{
    /**
     * Seed the application's database.
     *
     * @return void
     */
    public function run()
    {
        \App\Models\Invoice::factory(100)->create();
    }
}

Enter fullscreen mode Exit fullscreen mode

Acessando o terminal novamente vamos executar a migração.

php artisan migrate
Enter fullscreen mode Exit fullscreen mode

E logo em seguida geramos os registros no banco de dados.

php artisan db:seed
Enter fullscreen mode Exit fullscreen mode

Agora basta iniciar o servidor na porta 8100

php artisan serve --port=8100
Enter fullscreen mode Exit fullscreen mode

Testando a aplicação

Utilizando a extensão Thunder no VSCode faremos algumas requisições.

A primeira será para a rota que retorna todos os registros.

Note que temos exatamente 100.

Request to get all invoicesImage description

A segunda requisição retorna as informações de um registro passado como parâmetro.

Podemos ver que a coluna deleted_at está nula.

Request to get invoice id


Na segunda parte deste artigo criaremos os métodos para deletar, restaurar e consultar registros deletados.
Até breve.

💖 💪 🙅 🚩
marciopolicarpo
Marcio Policarpo

Posted on June 28, 2022

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related