Laravel, single table inheritance
Published: 2020-08-03 12:28:18

## Introdução

Olá pessoal, nesse artigo gostaria de compartilhar uma técnica de herança que utilizamos na ferramenta de [folha de pagamentos da Convenia](https://convenia.com.br/folha-de-pagamento) e que nos ajuda muito quando precisamos calcular coisas como totais e somas em tabelas.

Nossa aplicação de Folha é feita em Laravel então os exemplos que vocês vão ver são feitos em Laravel porém acredito que as técnicas podem ser reproduzidas em qualquer framework ou linguagem, não posso esquecer de ressaltar que para melhor compreensão os exemplos foram bem simplificados.

Implementar herança no seu design de classes é algo bem trivial, talvez uma das primeiras coisas que você deve aprender ao começar estudar orientação a objetos, mas quando essas heranças devem refletir no banco de dados (principalmente bancos de dados relacionais) logo você deve começar a perceber o quão desajeitado isso pode se tornar, isso em parte pelo fato dos bancos relacionais não terem um mecanismo de herança, normalmente o que se faz é linkar tabelas separadas que representam as classes.

Tanto para contextualizar como para aprofundamento acho importante citar as técnicas mais comuns de herança, que são [Concrete Table Inheritance](https://martinfowler.com/eaaCatalog/concreteTableInheritance.html), [Class Table Inheritance](https://martinfowler.com/eaaCatalog/classTableInheritance.html) e [Single Table Inheritance](https://www.martinfowler.com/eaaCatalog/singleTableInheritance.html), esta última será a que iremos explorar nesse artigo, ambas são explicadas com bem mais propriedade no livro [Patterns of Enterprise Application Architecture](https://martinfowler.com/books/eaa.html) do [Martin Fowler](https://martinfowler.com/aboutMe.html).

## Mão na massa

Para começar vamos mostrar uma das primeiras telas no passo a passo de fechamento, a tela de seleção de colaboradores.

![Tela de seleção](https://dev-to-uploads.s3.amazonaws.com/i/0bgk3zaawygoja8m7srf.png)

Nessa tela selecionamos quais colaboradores devem fazer parte do cálculo de fechamento, porém existe uma outra entidade que entra automáticamente no calculo do colaborador, o seu dependente, agora imagine que se esses registros fossem mantidos em tabelas separadas para pegar um simples "total de participantes" ou mesmo os "valores totais", teriamos que fazer muitos rodeios como "Joins" e até mesmo dois procedimentos separados e uma posterior soma já no lado do PHP, desajeitado certo? mas se isso fosse mantido em apenas uma tabela ajudaria bastante, dessa forma a implementação ficaria da seguinte forma:

```PHP

    public function up()

    {

        Schema::create('participants', function (Blueprint $table) {

            $table->id();

            $table->string('type');

            $table->unsignedInteger('payroll_id');

            $table->unsignedInteger('parent_id')->nullable();

            $table->string('name');

            $table->string('active')->default('inactive');

            $table->string('job_description')->nullable();

            $table->string('department')->nullable();

            $table->timestamps();

        });

    }

```

Esse é o código da nossa migração que gera a tabela `participants`, esse código já nos revela algo importante, teremos a entidade mais genérica `Participant`, além da entidade `Employee` e `Dependent` então o raciocínio é trazer todos os registros ao buscar com a model `Participant` e trazer os registros específicos ao buscas com as models mais específicas.

O colaborador tem todos os campos com exceção do campo `parent_id` já o dependente tem apenas o nome e o campo `parent_id`, importante notar que todo campo que é obrigatório apenas para uma entidade, não deve ser marcado como obrigatório no banco, isso torna o seu modelo de banco de dados um pouco mais frágil então a boa prática aqui é sempre manter a disciplina utilizando as mais altas abstrações para manipular os registros, evitar ao máximo alterar o banco diretamente, dessa forma pode ser muito interessante implementar até mesmo uma camada de serviços para as operações, na nossa versão de produção utilizamos serviços de forma opaca para cada operação que precisamos fazer, isso significa que salvo algumas exceções não chamamos a model diretamente.

O campo `type` é muito importante é ele que discrimina qual é o tipo do registro, é muito importante manter o preenchimento de campo de forma automática, para que não haja chance de esquecer em manipulações futuras.

```PHP
<?php

namespace App;

use Illuminate\Database\Eloquent\Relations\BelongsTo;

class Dependent extends Participant
{
    use InheritanceLock;

    protected $table = 'participants';

    protected $hidden = [
        'active',
        'job_description',
        'department'
    ];

    protected $fillable = [
        'payroll_id',
        'parent_id',
        'name',
    ];

    public function employee(): BelongsTo
    {
        return $this->belongsTo(Employee::class, 'parent_id');
    }
}
```

A model de `Dependent` é bem parecida com a model `Employee` com a exceção de que ela declara o relacionamento contrário, ela pertence a um employee.

Bem interessante notar também a declaração do array `$hidden` ele omite esses valores ao retornar uma model de dependente, os campos omitidos são referentes ao colaborador e não devem ser exibidos nos registros de dependentes

```PHP
<?php

namespace App;

use App\Scopes\InheritanceLockScope;

trait InheritanceLock
{
    public static function bootInheritanceLock(): void
    {
        $typeSetting = function ($model) {
            $model->type = self::class;
        };

        static::creating($typeSetting);
        static::saving($typeSetting);
        static::addGlobalScope(
            new InheritanceLockScope(self::class)
        );
    }
}
```

Você deve ter notado essa trait inserida nas models mais específicas, ela é responsável por preencher automáticamente o campo `type` e aplicar automáticamente o filtro pelo type em um [Global Scope](https://laravel.com/docs/7.x/eloquent#global-scopes)

O ciclo de vida e operações de uma model emitem eventos no Laravel, assim é possível escutar e declarar listeners para esses eventos, esse código declara um listener para os eventos de criação que preenche o campo `type` com `self::class`, esse é o nome da classe que "usa" a trait.

```PHP
<?php

namespace App\Scopes;

use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Scope;

class InheritanceLockScope implements Scope
{
    protected $type;

    public function __construct(string $type)
    {
        $this->type = $type;
    }

    /**
     * Apply the scope to a given Eloquent query builder.
     */
    public function apply(Builder $builder, Model $model): void
    {
        $builder->whereType($this->type);
    }
}
```

Essa é a classe do global scope, a função de um global scope é adicionar uma condição cada vez que acontece uma consulta, nesse caso cada vez que retornarmos os registros do banco ele deve adicionar um `WHERE type = "ModelName"` na consulta, isso nos garante que nunca vamos trazer um dependente no meio de colaboradores e vice versa.

Dessa forma nossa implementação está completa, a seguir a utilização:

![tinker inserção de dependentes e colaboradores](https://dev-to-uploads.s3.amazonaws.com/i/mlg0da52su964na4sd8m.png)

Como pode ser evidenciado no tinker criamos um colaborador e um dependente em sequencia e ao buscar os registros nas models específicas apenas os registros epecíficos são selecionados

![Dependentes e Colaboradores get all](https://dev-to-uploads.s3.amazonaws.com/i/qtm61tezcgqmx06exfuh.png)

Ao chamar a model mais generica conseguimos todos os registros

![relacionamentos](https://dev-to-uploads.s3.amazonaws.com/i/a6xcwcqpa8ge7z8xwkl9.png)

Os relacionamentos também funcionam como esperado

## Conclusão

Conseguimos ver os seguintes pontos positivos na utilização dessa técnica de herança:

- Existe apenas uma tabela para você se preocupar.
- Você não precisa fazer joins e unions para retornar os registros em uma única tacada.
- se você precisar adicionar um campo que ja existia em um tipo de model no outro tipo, não será necessário criar uma nova migração.
- Somas são bem facilitadas

Mas também devemos nos preocupar com os seguintes possíveis problemas:

- A confusão gerada ao olhar a tabela uma vez que alguns campos fazem sentido e outros não
- Colunas utilizadas apenas por um subtipo podem gastar espaço atoa dependendo do banco utilizado.
- A tabela pode acabar ficando muito grande e cheia de indexes, o que pode ser um problema de performance.
- E principalmente, a modelagem fica mais frágil na parte do banco de dados.

Na nossa experiência colhemos bons frutos com a utilização da técnica, cabe a você avaliar seus ganhos e implementar, ou não.

Espero ter contribuido de alguma forma!!