Pular para conteúdo

Padrão: Auto-geração de Código Sequencial

Este documento descreve o padrão implementado na branch feature/natureza-operacao-codigo-int-NH-243 para auto-gerar códigos sequenciais numéricos em entidades do sistema.

Problema Resolvido

Anteriormente, o campo Codigo de NaturezaOperacao era preenchido manualmente pelo usuário, o que causava: - Possibilidade de códigos duplicados - Códigos não numéricos inconsistentes - Necessidade de validação manual de unicidade

Solução Implementada

A solução utiliza uma sequence do PostgreSQL para gerar códigos numéricos automáticos e sequenciais.


Arquivos Alterados

Camada Arquivo Alteração
Business Interfaces/Cadastros/INaturezaOperacaoRepository.cs Novo método GetNextCodigo()
Business Services/Cadastros/NaturezaOperacaoService.cs Auto-gera código no Add()
Data Repositories/Cadastros/NaturezaOperacaoRepository.cs Implementação do GetNextCodigo()
Data Migrations/...ConvertNonNumericCodigosAndCreateSequence.cs Migration para criar sequence
WebApp ViewModels/NaturezaOperacaoViewModel.cs Campo Codigo nullable
WebApp Views/NaturezaOperacao/_Create.cshtml Campo oculto na criação, readonly na edição

Passo a Passo para Replicar

1. Criar a Migration

Crie uma migration para: 1. Normalizar dados existentes não numéricos (se aplicável) 2. Criar a sequence do PostgreSQL

make migration NAME=AddSequenceFor<Entidade>Codigo

Conteúdo da migration:

public partial class AddSequenceFor<Entidade>Codigo : Migration
{
    protected override void Up(MigrationBuilder migrationBuilder)
    {
        // Step 1: Converter códigos não numéricos (se necessário)
        migrationBuilder.Sql(@"
            DO $$
            DECLARE
                max_codigo INTEGER;
                next_codigo INTEGER;
                rec RECORD;
            BEGIN
                -- Obter o maior código numérico existente
                SELECT COALESCE(MAX(
                    CASE
                        WHEN ""Codigo"" ~ '^\d+$' THEN ""Codigo""::INTEGER
                        ELSE 0
                    END
                ), 0) INTO max_codigo
                FROM ""<NomeDaTabela>"";

                next_codigo := max_codigo + 1;

                -- Atualizar registros com códigos não numéricos
                FOR rec IN
                    SELECT ""Id""
                    FROM ""<NomeDaTabela>""
                    WHERE ""Codigo"" IS NULL
                       OR ""Codigo"" = ''
                       OR ""Codigo"" !~ '^\d+$'
                    ORDER BY ""Id""
                LOOP
                    UPDATE ""<NomeDaTabela>""
                    SET ""Codigo"" = next_codigo::TEXT
                    WHERE ""Id"" = rec.""Id"";

                    next_codigo := next_codigo + 1;
                END LOOP;
            END $$;
        ");

        // Step 2: Criar a sequence
        migrationBuilder.Sql(@"
            DO $$
            DECLARE
                max_codigo INTEGER;
            BEGIN
                SELECT COALESCE(MAX(""Codigo""::INTEGER), 0) + 1 INTO max_codigo
                FROM ""<NomeDaTabela>"";

                EXECUTE format('CREATE SEQUENCE IF NOT EXISTS ""<NomeDaTabela>_Codigo_seq"" START WITH %s', max_codigo);
            END $$;
        ");
    }

    protected override void Down(MigrationBuilder migrationBuilder)
    {
        migrationBuilder.Sql(@"DROP SEQUENCE IF EXISTS ""<NomeDaTabela>_Codigo_seq"";");
    }
}

2. Adicionar Método na Interface do Repository

Em Business/Interfaces/Cadastros/I<Entidade>Repository.cs:

public interface I<Entidade>Repository : IRepository<<Entidade>>
{
    // ... métodos existentes ...

    Task<string> GetNextCodigo();
}

3. Implementar no Repository

Em Data/Repositories/Cadastros/<Entidade>Repository.cs:

public async Task<string> GetNextCodigo()
{
    var connection = NelmetaisDbContext.Database.GetDbConnection();
    await connection.OpenAsync();

    using var command = connection.CreateCommand();
    command.CommandText = @"SELECT nextval('""<NomeDaTabela>_Codigo_seq""')";

    var result = await command.ExecuteScalarAsync();
    return result!.ToString()!;
}

4. Alterar o Service

Em Business/Services/Cadastros/<Entidade>Service.cs:

public async Task Add(<Entidade> entidade)
{
    // Auto-gerar Codigo da sequence
    entidade.Codigo = await _repository.GetNextCodigo();

    if (!RunValidation(new <Entidade>Validation(), entidade))
        return;

    // Remover validação de código duplicado (não é mais necessária)

    await _repository.Add(entidade);
}

public async Task Update(<Entidade> entidade)
{
    if (!RunValidation(new <Entidade>Validation(), entidade))
        return;

    // Remover validação de código duplicado no Update também

    await _repository.Update(entidade);
}

5. Atualizar o ViewModel

Em WebApp/Areas/Cadastros/ViewModels/<Entidade>ViewModel.cs:

// Remover [Required] do campo Codigo
// Tornar nullable
[StringLength(150, ErrorMessageResourceName = "MaxLength", ErrorMessageResourceType = typeof(DataAnnotationsResources))]
[Display(Name = "Código")]
public string? Codigo { get; set; }

6. Atualizar a View de Criação

Em WebApp/Areas/Cadastros/Views/<Entidade>/_Create.cshtml:

<div class="form-row">
    @if (Model.Id > 0)
    {
        <!-- Modo Edição: mostra código como readonly -->
        <div class="form-group col-md-6">
            <label asp-for="Codigo" class="control-label"></label>
            <input asp-for="Codigo" class="form-control" readonly />
        </div>
        <div class="form-group col-md-6">
            <label asp-for="Nome" class="control-label"></label>
            <input asp-for="Nome" class="form-control" />
        </div>
    }
    else
    {
        <!-- Modo Criação: não mostra campo código -->
        <div class="form-group col-md-12">
            <label asp-for="Nome" class="control-label"></label>
            <input asp-for="Nome" class="form-control" />
        </div>
    }
</div>

Aplicar Migration

make migrate

Checklist

  • [ ] Migration criada com normalização de dados + criação da sequence
  • [ ] Método GetNextCodigo() adicionado na interface do repository
  • [ ] Método GetNextCodigo() implementado no repository
  • [ ] Service alterado para auto-gerar código no Add()
  • [ ] Validação de código duplicado removida do Service
  • [ ] ViewModel com campo Codigo nullable e sem [Required]
  • [ ] View de criação ocultando campo ou mostrando como readonly
  • [ ] Migration aplicada com make migrate
  • [ ] Testes manuais realizados

Observações

  1. Sequence Name Convention: Use o padrão "<NomeDaTabela>_Codigo_seq" para facilitar identificação
  2. Dados Existentes: A migration normaliza automaticamente códigos não numéricos
  3. Rollback: O Down da migration remove a sequence, mas não restaura valores originais não numéricos
  4. Concorrência: Sequences do PostgreSQL são thread-safe, garantindo unicidade mesmo em acessos concorrentes