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
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
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
Codigonullable e sem[Required] - [ ] View de criação ocultando campo ou mostrando como readonly
- [ ] Migration aplicada com
make migrate - [ ] Testes manuais realizados
Observações
- Sequence Name Convention: Use o padrão
"<NomeDaTabela>_Codigo_seq"para facilitar identificação - Dados Existentes: A migration normaliza automaticamente códigos não numéricos
- Rollback: O Down da migration remove a sequence, mas não restaura valores originais não numéricos
- Concorrência: Sequences do PostgreSQL são thread-safe, garantindo unicidade mesmo em acessos concorrentes