Gerador de IDs sequenciais com reinicialização anual — Suporte a múltiplos prefixos

Prev Next

To download the Yearly Resetting Sequential ID Generator - Multi-Prefix Support, visit: [tulip](https://<span translate=).co/library/apps/yearly-resetting-sequential-id-generator---multi-prefix-support/">Library

Neste artigo, um de nossos desenvolvedores da biblioteca, Tamás, explica as decisões de desenvolvimento por trás do aplicativo de bloco de construção “Gerador de ID sequencial com redefinição anual — Suporte a múltiplos prefixos”, disponível na biblioteca.

Por que eu o criei dessa forma

Projetei esse bloco lógico para resolver um problema muito prático: gerar IDs de maneira confiável, fácil de reutilizar e rápida de implantar em diversos aplicativos.Meu objetivo era que o próprio ID contivesse tudo o que fosse necessário para calcular o próximo. Cada parte do ID tem uma função clara: * o prefixo indica a qual sequência o ID pertence; * o ano separa a sequência ao longo do tempo; * o número mostra a ordem dentro desse ano

Devido a essa estrutura, não precisei introduzir nenhuma lógica de reinicialização. O ID mais recente já contém todas as informações necessárias para gerar o próximo. Desde que o último ID seja válido, o próximo ID sempre pode ser calculado de maneira previsível.

Incluí o ano diretamente no ID porque ele separa naturalmente os registros ao longo do tempo. Isso também permite que a numeração recomece automaticamente a cada ano, sem a necessidade de qualquer reinicialização programada ou atualização manual.

O prefixo atua como um limite entre diferentes sequências. Isso possibilita a execução de múltiplas sequências de numeração independentes dentro da mesma tabela. Alterar o prefixo significa simplesmente dar continuidade a uma sequência diferente.

Adicionei a validação porque toda a abordagem depende de uma estrutura de ID consistente. Se um ID existente não seguir o formato esperado, continuar a sequência pode levar a resultados confusos ou inconsistentes. Prefiro interromper logo no início, tornar o problema visível e evitar a geração de IDs incorretos.

Optei por uma consulta baseada em conector para que o bloco de lógica possa funcionar com diferentes tabelas sem alterar a lógica central. Uma vez que o conector esteja configurado, basta fornecer o tableId e o prefixo, e o mesmo bloco de lógica pode ser reutilizado em vários lugares.

Para mim, o objetivo não era apenas gerar IDs, mas criar um padrão reutilizável em que o próprio formato do ID defina como a sequência funciona. É isso que mantém a lógica simples, previsível e fácil de reutilizar em diferentes aplicativos.

O que este bloco lógico faz

Este bloco lógico gera IDs sequenciais que são reiniciados anualmente com a seguinte estrutura:prefixo + ano + número sequencial

Exemplo: TEST20240001 TEST20240002

Quando o ano muda, a numeração recomeça automaticamente do 1: TEST20250001

Podem existir vários prefixos na mesma tabela. Cada prefixo mantém sua própria sequência independente: ORD20240001 ORD20240002 INV20240001 INV20240002

O bloco lógico garante: * formato consistente de ID * reinicialização anual automática * numeração independente por prefixo * detecção precoce de IDs históricos com formato incorreto * detecção precoce de estouro numérico

Por que o conector faz parte do projeto

Antes de gerar o próximo ID, o bloco lógico precisa do ID mais recente existente para o prefixo em questão. O conector realiza uma pesquisa filtrada: * filtra os registros cujo ID começa com o prefixo * ordena em ordem decrescente por _sequenceNumber * retorna o registro mais recente

Como o prefixo faz parte do filtro, cada prefixo possui seu próprio contador contínuo, mesmo dentro da mesma tabela. Isso mantém as sequências isoladas e previsíveis.

Como a construção está estruturada

A implementação real utiliza três gatilhos. Essa estrutura é intencional. Eu queria que o bloco lógico fosse fácil de acompanhar de cima para baixo, mantendo ao mesmo tempo a lógica principal separada e reutilizável. Assim, em vez de distribuir a lógica por vários gatilhos editáveis, organizei-a em três etapas:1. CONFIGURAÇÃO DO USUÁRIO: ETAPA DE ENTRADA 2. Validação MAGIC + geração de ID 3. CONFIGURAÇÃO DO USUÁRIO: ETAPA DE SAÍDA

Isso proporciona um bom equilíbrio entre usabilidade e controle.* O primeiro gatilho é onde você define as entradas.* O segundo gatilho contém a lógica interna que normalmente não precisa ser editada para que o bloco lógico funcione.* O terceiro gatilho é onde você decide o que fazer com o resultado gerado.

Isso significa que, normalmente, você não precisa mexer no gatilho da lógica central. Basta configurar o lado da entrada e o lado da saída. Essa foi uma das principais decisões de design: tornar o bloco lógico reutilizável, expondo apenas as partes que geralmente precisam ser alteradas.

Gatilho 1 – CONFIGURAÇÃO DO USUÁRIO: ESTÁGIO DE ENTRADA

Este é o primeiro gatilho com o qual você interage. Seu objetivo é definir os valores de entrada de que o bloco lógico precisa antes que qualquer ID possa ser gerado. Na prática, esse gatilho é usado para definir:* O prefixo: _configPrefix* Comprimento do ID: _configIdNumberFormat* O ID da tabela passado para o conector: tableId

O que cada valor significa

**_configPrefix:**é o texto colocado no início de cada ID gerado. Você o utiliza para definir qual sequência deseja gerar.

Exemplos:* ORD* INV* TEST

Se _configPrefix = ORD, o bloco lógico considera apenas os IDs existentes que começam com ORD e dá continuidade apenas a essa sequência. Portanto, esse valor realiza duas funções ao mesmo tempo:* define o prefixo visível no ID final* define quais IDs históricos o conector deve usar como fonte para o próximo número

Isso significa que prefixos diferentes na mesma tabela geram sequências diferentes**._configIdNumberFormat**

Isso define o comprimento da parte numérica do ID. Você o define como uma string preenchida com zeros. Exemplos:000000000000000000

O número de zeros define a largura da seção numérica.

Exemplos:0000 → 0001, 0002, 0003 ... 9999 000000 → 000001, 000002 ... 999999

Esse valor não define o número em si. Ele define o formato no qual o número deve se encaixar. Eu o utilizo para dois fins: * para adicionar zeros à esquerda ao número gerado * para validar se o próximo número ainda se encaixa no comprimento permitido

tableId

Isso define qual tabela o conector deve consultar.

Na prática, é isso que torna o bloco lógico reutilizável em diferentes tabelas. Você pode copiar o mesmo bloco lógico e direcioná-lo para outra tabela alterando apenas esse valor no gatilho de configuração.

O bloco lógico não pesquisa tabelas por conta própria. Ele passa tableId e _configPrefix para o conector, e o conector retorna o ID correspondente mais recente dessa tabela.

Como usá-los juntos

Esses três valores definem, em conjunto, o comportamento do gerador:* tableId indica ao bloco lógico onde procurar* _configPrefix indica a qual sequência ele deve dar continuidade* _configIdNumberFormat define como a parte numérica deve ser apresentada

Orientações práticas de configuração

Ao configurar o bloco lógico, certifique-se de que:* o tableId aponte para a tabela correta* o _configPrefix corresponda ao prefixo que você deseja continuar* o _configIdNumberFormat corresponda ao comprimento do número que você deseja aplicar

Se você alterar o prefixo, não estará mais dando continuidade à sequência antiga. Você estará iniciando ou dando continuidade a uma sequência diferente, específica para esse prefixo. Se você aumentar o comprimento do formato numérico posteriormente, o bloco lógico esperará esse formato mais amplo ao validar IDs existentes nessa tabela e no escopo do prefixo.O conector recupera o ID mais recente que corresponde ao prefixo e o armazena em _stateLastId. Se ainda não houver nenhum ID, LINK() garante que o gerador ainda possa produzir o primeiro ID válido.

Por que o conector faz parte do projeto

O conector não é apenas um detalhe técnico. Ele faz parte da estratégia de reutilização. Eu uso o conector para ler o ID mais recente existente na tabela de destino. O conector recebe o ID da tabela como entrada, aplica o filtro de prefixo, ordena os registros por _sequenceNumber em ordem decrescente e retorna o registro correspondente mais recente.

Isso é importante por dois motivos.

Primeiro, permite que o bloco lógico continue a numeração a partir do estado real da tabela.

Segundo, torna o bloco lógico muito mais fácil de reutilizar. Se você copiar o botão para outro aplicativo, não precisará reescrever a lógica. Basta apontar o conector para a tabela correta e fornecer a configuração necessária. Esse é o padrão prático por trás da construção: * crie o botão uma única vez * defina a entrada da tabela no conector * defina o prefixo e o formato do número * reutilize-o sempre que necessário

Essa foi uma escolha deliberada para facilitar a criação mais rápida de aplicativos.

O que o conector faz no bloco de lógica?

O que acontece aqui:* o conector recebe o ID da tabela* o conector recebe o prefixo* o conector filtra os registros pelo prefixo* o conector ordena os registros por _sequenceNumber em ordem decrescente* o conector retorna o ID correspondente mais recente

O conector também oferece suporte a um caso de uso muito importante: múltiplas entidades na mesma tabela.

Se a mesma tabela contiver registros como: ORD20240001 MAT20240011

então o bloco lógico não deve tratá-los como uma sequência compartilhada. Cada prefixo deve continuar apenas a partir de seu próprio valor mais recente.

Portanto, o conector filtra primeiro por prefixo. Isso significa que os próximos valores passam a ser: ORD → ORD20240002 MAT → MAT20240012

Escolhi esse modelo porque entidades diferentes podem coexistir na mesma tabela, mas isso não significa que devam compartilhar o mesmo número sequencial.

Também vale a pena deixar claro um requisito de configuração: o conector precisa de autenticação, portanto, um token deve ser gerado e adicionado às configurações de autenticação do conector. Considero isso um trabalho de configuração, não parte da lógica do próprio bloco lógico, mas ainda assim é um aspecto importante para tornar o bloco lógico utilizável.

Gatilho 2 – MAGIC: validação + geração de ID

Este gatilho é a lógica central do bloco lógico. Ele realiza três ações em sequência: 1. verifica se o ID correspondente mais recente possui a estrutura esperada; 2. verifica se o próximo número ainda se encaixaria no comprimento numérico configurado; 3. gera o próximo ID

Agrupei essas etapas em um único gatilho porque elas pertencem ao mesmo fluxo de decisão. Antes de criar um novo ID, quero responder a três perguntas: * o ID anterior é estruturalmente seguro para uso? * o próximo número ainda se encaixaria no formato? * se sim, qual deveria ser o próximo ID?

Validação de formato

Expression

REGEX_REPLACE(LINK(Variable._stateLastId.id, Variable._configPrefix + DATETIMETOTEXT(App info.Current Date and Time,'YYYY') + Variable._configIdNumberFormat),'^' + Variable._configPrefix + '[0-9]{4}' + '[0-9]{' + LEN(Variable._configIdNumberFormat) + '}$','ok')

O que este expression faz

Este expression verifica se o ID mais recente corresponde exatamente à estrutura esperada pelo bloco lógico. Ele funciona nesta ordem: 1. LINK(...) utiliza Variable._stateLastId.id quando o conector retorna um valor. 2. Se ainda não houver nenhum ID, LINK(...) usa um valor de fallback construído a partir de: * Variable._configPrefix * DATETIMETOTEXT(...,'YYYY') * Variable._configIdNumberFormat3. Esse valor de fallback é incluído para que a validação ainda tenha uma estrutura válida para avaliar, mesmo quando este for o primeiro ID gerado.4. Variable._configPrefix define o prefixo exato com o qual o ID deve começar.5. DATETIMETOTEXT(...,'YYYY') não é usado como um valor de correspondência dentro da própria expressão regular, mas faz parte da estrutura de fallback para que este se pareça com um formato de ID real.6. LEN(Variable._configIdNumberFormat) define quantos dígitos a parte numérica deve conter.7. REGEX_REPLACE(...) então compara a string completa com este padrão: * prefixo * exatamente 4 dígitos para o ano * exatamente N dígitos para a parte numérica8. Se a string completa corresponder, todo o valor é substituído por “ok”.9. Se não corresponder, o valor original permanece inalterado.10. O gatilho verifica então se o resultado é igual a “ok”. Se não for, a execução é interrompida.

Portanto, este expression não está extraindo valores para cálculo. Sua função é verificar se o ID possui a estrutura esperada antes que qualquer lógica numérica seja aplicada.

Por que escolhi isso

Optei por validar a estrutura completa do ID antes de aplicar qualquer lógica numérica. Criei-o dessa forma porque o restante do gatilho pressupõe posições fixas:* prefixo no início* ano imediatamente após o prefixo* número imediatamente após o ano

Se o ID anterior não seguir esse padrão, incrementá-lo a partir dele seria pouco confiável.

Também optei por incluir o fallback LINK(...) aqui para que o primeiro ID gerado não precise de uma lógica de validação separada. O mesmo caminho de validação funciona independentemente de existir ou não um ID mais antigo. Eu interrompo intencionalmente o gatilho em vez de tentar corrigir IDs malformados. A correção automática ocultaria problemas nos dados e poderia criar uma sequência que pareça válida, mas que seja construída com base em uma entrada incorreta.

Proteção contra estouro

Expression

LEN(IF(MID(LINK(Variable._stateLastId.id, Variable._configPrefix + Variable._configIdNumberFormat), LEN(Variable._configPrefix), 4) = DATETIMETOTEXT(App info.Data e hora atuais,'AAAA'),RIGHT(Variável._configIdNumberFormat + TOTEXT(TEXTTOINTEGER(REGEX_REPLACE(LINK(Variável._stateLastId.id, Variável._configPrefix + Variável._configIdNumberFormat),'^' + Variable._configPrefix + '[0-9]{4}','')) + 1), LEN(Variable._configIdNumberFormat)), RIGHT(Variable._configIdNumberFormat + '1', LEN(Variable._configIdNumberFormat))))

O que este expression faz

Este expression calcula qual seria a próxima parte numérica e, em seguida, mede seu comprimento. Ele funciona nesta ordem: 1. LINK(...) usa o último ID, quando houver um. Caso contrário, recorre a Variable._configPrefix + Variable._configIdNumberFormat. 2. MID(..., LEN(Variable._configPrefix), 4) lê os 4 caracteres imediatamente após o prefixo. Isso é tratado como a parte do ano do ID anterior. 3. DATETIMETOTEXT(...,'YYYY') fornece o ano atual para comparação. 4. IF(...) decide se a parte numérica deve continuar ou ser reiniciada: * mesmo ano → incremento * ano diferente → recomeça a partir de 15. No ramo de incremento, REGEX_REPLACE(...) remove o prefixo e o ano de 4 dígitos do início do ID anterior, de modo que apenas a parte numérica permaneça.6. TEXTTOINTEGER(...) + 1 converte esse texto numérico em um número e o incrementa.7. TOTEXT(...) converte o novo número de volta em texto.8. Variable._configIdNumberFormat + ... coloca o formato de zeros configurado à frente do valor.9. RIGHT(..., LEN(Variable._configIdNumberFormat)) mantém apenas o número necessário de dígitos.10. No ramo de reinicialização, RIGHT(Variável._configIdNumberFormat + '1', LEN(...)) cria o primeiro número permitido para um novo ano.11. LEN(...) mede o comprimento da parte numérica resultante.

O gatilho compara esse comprimento com o comprimento permitido definido em Variable._configIdNumberFormat. Se a próxima parte numérica for muito longa, a execução é interrompida.

Por que escolhi isso

Optei por calcular o próximo número da mesma forma que o ID é realmente gerado, em vez de usar uma fórmula separada para estimar quando o número excederia o comprimento permitido.

Criei dessa forma porque queria que a verificação de estouro seguisse exatamente as mesmas regras da geração real do ID. Isso reduz a chance de a lógica de validação e a lógica de geração se distanciarem posteriormente.

Parar aqui é mais seguro do que continuar. Se o próximo valor numérico não couber mais no comprimento configurado, o ID resultante quebraria a estrutura que você configurou.

Por exemplo, se o formato numérico for 0000, então 9999 é válido, mas 10000 não é. Quero que o bloco lógico detecte isso antes da criação do registro, e não depois.

Lógica de geração de ID

Expression

Variável._configPrefix + DATETIMETOTEXT(Informações do aplicativo.Data e hora atuais,'AAAA')+ IF(MID(LINK(Variável._stateLastId.id, Variável._configPrefix + Variável._configIdNumberFormat),LEN(Variável._configPrefix),4) = DATETIMETOTEXT(Informações do aplicativo.Data e hora atuais, 'AAAA'), RIGHT(Variável._configIdNumberFormat + TOTEXT(TEXTTOINTEGER(REGEX_REPLACE(LINK(Variável._stateLastId.id, Variável._configPrefix + Variável._configIdNumberFormat),'^' + Variable._configPrefix + '[0-9]{4}','')) + 1), LEN(Variable._configIdNumberFormat)), RIGHT(Variable._configIdNumberFormat + '1', LEN(Variable._configIdNumberFormat)))

O que este expression faz

Esta é a função expression que constrói o ID final. Ela funciona nesta ordem: 1. **Variable.\_configPrefix** adiciona o prefixo configurado no início do ID. 2. **DATETIMETOTEXT(App info.Current Date and Time,'YYYY')** adiciona o ano atual com 4 dígitos. 3. LINK(...) utiliza o último ID retornado pelo conector, quando houver um. Se o conector não retornar um ID, ele usa Variable._configPrefix + Variable._configIdNumberFormat como estrutura alternativa. 4. MID(..., LEN(Variable._configPrefix), 4) lê os 4 caracteres imediatamente após o prefixo. Esses caracteres são tratados como a parte do ano do ID anterior.5. DATETIMETOTEXT(...,'YYYY') é usado novamente aqui para que o expression possa determinar se o ID anterior pertence ao ano atual.6. IF(...) controla os dois caminhos possíveis: * mesmo ano → continuar a numeração * ano diferente → reiniciar em 17. No ramo do mesmo ano, REGEX_REPLACE(...) remove o prefixo e o ano de 4 dígitos do início do ID anterior, de modo que apenas a parte numérica permaneça.8. TEXTTOINTEGER(...) + 1 ** incrementa esse valor numérico.9. TOTEXT (...) converte o valor incrementado de volta em texto.10. Variable._configIdNumberFormat + ... coloca o formato de zero configurado à frente do novo valor.11. RIGHT (..., LEN(Variable._configIdNumberFormat)) mantém apenas o número necessário de dígitos, de modo que a parte numérica sempre tenha comprimento fixo.12. No ramo de reinicialização, RIGHT (Variável._configIdNumberFormat + '1', LEN(...))** cria o primeiro número de uma nova sequência anual.13. O prefixo, o ano atual e o número formatado são concatenados para formar o ID final.

Portanto, a saída final sempre tem esta estrutura:prefixo + ano + número de comprimento fixo

Por que escolhi isso

Optei por derivar o próximo ID inteiramente a partir do ID real mais recente na tabela. Criei dessa forma porque o ID anterior já contém tudo o que a lógica precisa: * a qual prefixo a sequência pertence * a qual ano ela pertence * qual foi o último valor numérico

Isso significa que não preciso de uma tabela de contador separada, de um campo de ano armazenado nem de uma reinicialização anual programada.

Também optei pela formatação de comprimento fixo porque ela mantém a estrutura previsível e mais fácil de validar.

A desvantagem é que o “ expression ” depende de uma estrutura rígida de IDs. É por isso que a etapa de validação vem primeiro. Aceitei essa desvantagem porque um contrato rígido e legível é mais útil aqui do que tentar suportar muitos formatos de entrada flexíveis.

Gatilho 3 – CONFIGURAÇÃO DO USUÁRIO: ESTÁGIO DE SAÍDA

Este é o último gatilho no bloco lógico. Seu objetivo é definir o que deve acontecer com o ID gerado assim que _stateOutput contiver um valor válido.

O ponto importante aqui é que essa etapa foi deixada intencionalmente em aberto para você.

Se _stateOutput não estiver em branco, você pode decidir o que o bloco lógico deve fazer a seguir. Na configuração atual, o gatilho exibe uma mensagem de orientação que explica as próximas etapas: * adicionar a tabela configurada ao aplicativo * substituir a etapa de mensagem por um gatilho “Criar Registro” * usar _stateOutput como o ID do novo registro

Eu o criei dessa forma porque o comportamento da saída pode variar entre os casos de uso.

Você pode querer: * criar um registro imediatamente * armazenar o ID em outra variável primeiro * usar o ID dentro de um fluxo de trabalho mais longo * validar condições adicionais antes de gravar na tabela

Em vez de definir um comportamento de saída fixo, mantive o estágio de saída configurável.

É por isso que a configuração voltada para o usuário existe em ambas as extremidades do bloco lógico:* a estrutura de entrada é configurável* o comportamento de saída é configurável* a lógica central permanece no meio

Abordagem para nomenclatura de variáveis

As variáveis internas usam o prefixo de sublinhado:* _configPrefix* _configIdNumberFormat* _stateLastId* _stateOutput

Isso ajuda a distinguir rapidamente entre valores de configuração e variáveis de processamento interno.

Limites do projeto

O bloco lógico assume esta estrutura:prefixo + ano + número. Cadaprefixo mantém sua própria sequência independente.

O bloco lógico não suporta um contador compartilhado entre vários prefixos.

Se as seções de ano ou numéricas forem movidas dentro do ID, o expressions deve ser ajustado de acordo.

Resumo das decisões de desenvolvimento

Se eu reduzir todo o bloco lógico às decisões centrais por trás dele, elas são estas:* Eu queria reutilização rápida, então o bloco lógico é orientado por configuração.* Eu queria que a tabela fosse a fonte de verdade, então o próximo ID é sempre baseado no registro real mais recente.* Eu queria que a lógica permanecesse fácil de manter, por isso o cálculo está centralizado em um único local.* Eu queria que várias entidades em uma tabela funcionassem de maneira organizada, por isso cada prefixo tem sua própria sequência independente.* Eu queria que a estrutura do ID permanecesse consistente ao longo do tempo, por isso o ano faz parte do ID e a sequência é reiniciada automaticamente quando o ano muda.* Eu queria proteger a tabela contra padrões inadequados; por isso, a validação de formato e as verificações de estouro impedem IDs incorretos antes que um registro seja criado.* Eu queria que o fluxo do gatilho permanecesse legível; por isso, as responsabilidades são separadas em etapas claras: configuração, validação, geração e saída.

É por isso que o bloco lógico tem essa aparência. Não se trata apenas de um gerador de IDs. É um padrão reutilizável para a criação controlada de IDs que busca equilibrar simplicidade, reutilização e proteção de dados, mantendo a segmentação anual incorporada diretamente à estrutura do ID.

Participe

Junte-se à comunidade para compartilhar melhorias, propor unidades adicionais, relatar problemas ou discutir decisões arquitetônicas.