Evolução de esquema segura frente à regeneração para migrações previsíveis
Evolução de esquema segura frente à regeneração mantém os dados de produção válidos quando o código do backend é regenerado. Aprenda uma maneira prática de planejar mudanças de esquema e migrações.

Por que mudanças de esquema parecem arriscadas quando o backend é regenerado
Quando seu backend é regenerado a partir de um modelo visual, uma alteração de banco de dados pode parecer puxar um fio de um suéter. Você atualiza um campo no Data Designer, clica em regenerar, e de repente não está apenas mudando uma tabela. Você também está mudando a API gerada, regras de validação e as queries que seu app usa para ler e escrever dados.
O que geralmente dá errado não é que o novo código não compile. Muitas plataformas sem código (incluindo AppMaster, que gera código Go real para backends) geram um projeto limpo toda vez. O risco real é que já exista dado em produção, e ele não se reestrutura automaticamente para corresponder às suas novas ideias.
As duas falhas que as pessoas notam primeiro são simples:
- Leituras quebradas: o app não consegue carregar registros porque uma coluna mudou de lugar, o tipo mudou ou uma query espera algo que não existe.
- Escritas quebradas: novos ou atualizados registros falham porque restrições, campos obrigatórios ou formatos mudaram, enquanto clientes antigos ainda enviam a forma antiga.
Ambas doem porque podem ficar ocultas até que usuários reais as encontrem. Um banco de staging pode estar vazio ou recém-semeado, então tudo parece bem. Produção tem casos de borda: nulls onde você supôs valores, strings antigas de enum, ou linhas criadas antes de existir uma regra nova.
É por isso que a evolução de esquema segura frente à regeneração importa. O objetivo é tornar cada mudança segura mesmo quando o código do backend é totalmente regenerado, para que registros antigos permaneçam válidos e novos registros ainda possam ser criados.
“Migrações previsíveis” significa apenas que você consegue responder quatro perguntas antes de implantar: o que mudará no banco, o que acontecerá com as linhas existentes, qual versão do app pode continuar funcionando durante o rollout e como você fará rollback se algo inesperado aparecer.
Um modelo simples: esquema, migrações e código regenerado
Quando sua plataforma pode regenerar o backend, ajuda separar três coisas na cabeça: o esquema do banco, os passos de migração que o alteram e os dados vivos já em produção. Confundir esses itens é por que mudanças parecem imprevisíveis.
Pense em regeneração como “reconstruir o código da aplicação a partir do modelo mais recente”. Em uma ferramenta como AppMaster, essa reconstrução pode acontecer muitas vezes durante o trabalho normal: você ajusta um campo, modifica a lógica, adiciona um endpoint, regenera, testa, repete. A regeneração é frequente. Seu banco de produção não deveria ser.
Aqui está o modelo simples.
- Schema: a estrutura das tabelas, colunas, índices e constraints. É o que o banco espera.
- Migrations: os passos ordenados e repetíveis que movem o esquema de uma versão para a outra (e às vezes transformam dados também). É o que você executa em cada ambiente.
- Runtime data: os registros reais criados por usuários e processos. Devem permanecer válidos antes, durante e depois da mudança.
O código regenerado deve ser tratado como “o app atual que conversa com o esquema atual”. As migrações são a ponte que mantém esquema e dados em runtime alinhados enquanto o código muda.
Por que a regeneração muda as regras do jogo
Se você regenera com frequência, naturalmente fará muitas pequenas edições no esquema. Isso é normal. O risco aparece quando essas edições implicam uma mudança no banco que não é compatível retroativamente, ou quando sua migração não é determinística.
Uma maneira prática de gerenciar isso é planejar a evolução de esquema segura frente à regeneração como uma série de passos pequenos e reversíveis. Em vez de um grande corte, faça movimentos controlados que mantenham caminhos antigos e novos funcionando juntos por um curto período.
Por exemplo, se você quer renomear uma coluna usada por uma API em produção, não a renomeie imediatamente. Primeiro adicione a nova coluna, escreva em ambas, faça backfill das linhas existentes, depois mude as leituras para a nova coluna. Só então remova a coluna antiga. Cada passo é fácil de testar e, se algo der errado, você pode pausar sem corromper dados.
Esse modelo mental torna as migrações previsíveis, mesmo quando a regeneração de código ocorre diariamente.
Tipos de mudanças de esquema e quais quebram produção
Quando seu backend é regenerado a partir do esquema mais recente, o código geralmente assume que o banco corresponde a esse esquema agora. Por isso, evolução de esquema segura frente à regeneração é menos sobre “podemos mudar o banco?” e mais sobre “dados antigos e requisições antigas conseguem sobreviver enquanto aplicamos a mudança?”.
Algumas mudanças são naturalmente seguras porque não invalidam linhas ou queries existentes. Outras mudam o significado dos dados ou removem algo que o app em execução ainda espera — é aí que ocorrem incidentes em produção.
Baixo risco, geralmente seguro (aditivo)
Mudanças aditivas são as mais fáceis de enviar porque podem conviver com dados antigos.
- Nova tabela que nada depende ainda.
- Nova coluna nullable sem requisito de default.
- Novo campo de API que é opcional de ponta a ponta.
Exemplo: adicionar uma coluna nullable middle_name à tabela users normalmente é seguro. Linhas existentes permanecem válidas, o código regenerado pode lê-la quando presente, e linhas antigas simplesmente têm NULL.
Risco médio (mudanças de significado)
Essas mudanças frequentemente “funcionam” tecnicamente, mas quebram comportamentos. Precisam de coordenação porque a regeneração atualiza validações, modelos gerados e suposições da lógica de negócio.
Renomes são a armadilha clássica: renomear phone para mobile_phone pode regenerar código que não lê mais phone, enquanto produção ainda tem dados lá. Da mesma forma, mudar unidades (preço em dólares versus centavos) pode corromper cálculos silenciosamente se você atualizar o código antes dos dados, ou os dados antes do código.
Enums são outra aresta cortante. Apertar um enum (remover valores) pode tornar linhas existentes inválidas. Expandir um enum (adicionar valores) costuma ser seguro, mas apenas se todos os caminhos de código souberem lidar com o novo valor.
Uma abordagem prática é tratar mudanças de significado como “adicionar novo, backfill, alternar, depois remover”.
Alto risco (destrutivo)
Mudanças destrutivas são as que mais frequentemente quebram produção imediatamente, especialmente quando a plataforma regenera código que deixa de esperar a forma antiga.
Remover uma coluna, dropar uma tabela ou mudar uma coluna de nullable para not-null pode fazer escritas falharem assim que qualquer requisição tentar inserir uma linha sem aquele valor. Mesmo que você ache que “todas as linhas já têm”, o próximo caso de borda ou job em background pode provar o contrário.
Se você precisa fazer uma mudança para not-null, faça em fases: adicione a coluna como nullable, faça backfill, atualize a lógica do app para sempre preenchê-la, então imponha not-null.
Mudanças de performance e segurança (podem bloquear escritas)
Índices e constraints não são mudanças de “forma de dados”, mas podem causar downtime. Criar um índice em uma tabela grande ou adicionar uma constraint UNIQUE pode bloquear escritas tempo suficiente para causar timeouts. No PostgreSQL, certas operações são mais seguras quando feitas com métodos que não bloqueiam, mas o ponto principal é o timing: faça operações pesadas em períodos de baixo tráfego e meça quanto tempo levam numa cópia de staging.
Quando mudanças precisam de cuidado extra em produção, planeje para:
- Um rollout em dois passos (esquema primeiro, código depois, ou vice-versa) que permaneça compatível.
- Backfills que rodem em lotes.
- Um caminho claro de rollback (o que acontece se o backend regenerado for ao ar cedo demais).
- Queries de verificação que provem que os dados batem com as regras novas.
- Um ticket “remover campo antigo depois” para que a limpeza não aconteça no mesmo deploy.
Se você usa uma plataforma como AppMaster que regenera código do Data Designer, a mentalidade mais segura é: envie mudanças que os dados antigos possam conviver hoje, e só então aperte as regras quando o sistema já tiver se adaptado.
Princípios para mudanças seguras frente à regeneração
Backends regenerados são ótimos — até uma mudança de esquema chegar em produção e linhas antigas não baterem com a forma nova. O objetivo da evolução de esquema segura frente à regeneração é simples: manter seu app funcionando enquanto o banco e o código regenerado acompanham um ao outro em passos pequenos e previsíveis.
Padrão: “expandir, migrar, reduzir”
Trate cada mudança significativa como três movimentos. Primeiro, expanda o esquema para que tanto o código antigo quanto o novo possam rodar. Depois migre os dados. Só então contraia removendo colunas, defaults ou constraints antigas.
Uma regra prática: nunca combine “nova estrutura” com “limpeza destrutiva” no mesmo deploy.
Suporte às formas antiga e nova por um tempo
Assuma que haverá um período em que:
- alguns registros têm os campos novos, outros não
- algumas instâncias do app rodam código antigo, outras o código regenerado
- jobs em background, imports ou clientes móveis podem ficar atrasados
Projete seu banco para que ambas as formas sejam válidas durante esse overlap. Isso importa ainda mais quando uma plataforma regenera seu backend a partir do modelo mais recente (por exemplo, no AppMaster quando você atualiza o Data Designer e regenera o backend em Go).
Torne leituras compatíveis antes das escritas
Comece fazendo o novo código capaz de ler dados antigos com segurança. Só então mude os caminhos de escrita para produzir o novo formato.
Por exemplo, se você dividir um único campo status em status + status_reason, entregue código que consiga lidar com status_reason ausente primeiro. Depois, comece a gravar status_reason em novas atualizações.
Decida o que fazer com dados parciais e desconhecidos
Ao adicionar enums, colunas não-null ou constraints mais rígidas, decida antecipadamente o que acontece quando valores estiverem faltando ou inesperados:
- permitir nulls temporariamente e depois backfill
- definir um default seguro que não mude o significado
- manter um valor “unknown” para evitar falhas de leitura
Isso previne corrupção silenciosa (defaults errados) e falhas duras (novas constraints rejeitando linhas antigas).
Tenha um plano de rollback para cada passo
Rollback é mais fácil durante a fase de expansão. Se for necessário reverter, o código antigo ainda deve funcionar contra o esquema expandido. Anote o que você reverteria (apenas código, ou código mais migração) e evite mudanças destrutivas até ter confiança de que não precisará desfazer.
Passo a passo: planeje uma mudança que sobreviva à regeneração
Backends regenerados são implacáveis: se o esquema e o código gerado discordarem, a produção geralmente detecta primeiro. A abordagem mais segura é tratar cada mudança como um rollout pequeno e reversível, mesmo se você estiver construindo com ferramentas sem código.
Comece escrevendo a intenção em linguagem simples e como seus dados estão hoje. Escolha 3 a 5 linhas reais de produção (ou um dump recente) e note os pontos problemáticos: valores vazios, formatos antigos, defaults surpreendentes. Isso evita projetar um campo novo perfeito que os dados reais não suportam.
Aqui está uma sequência prática que funciona bem quando sua plataforma regenera o backend (por exemplo, quando o AppMaster regenera serviços backend em Go a partir do modelo do Data Designer):
-
Expandir primeiro, não substituir. Adicione novas colunas ou tabelas de forma aditiva. Faça os novos campos nullable inicialmente, ou dê defaults seguros. Se estiver introduzindo um novo relacionamento, permita a FK vazia até populá-la.
-
Deploy do esquema expandido sem remover nada. Aplique a mudança de banco enquanto o código antigo ainda funciona. O objetivo é: o código antigo pode continuar escrevendo as colunas antigas, e o banco as aceita.
-
Backfill em um job controlado. Popule campos novos usando um processo em lotes que você possa monitorar e rerodar. Torne-o idempotente (rodar duas vezes não deve corromper dados). Faça gradual se a tabela for grande e registre quantas linhas foram atualizadas.
-
Alterne leituras primeiro, com fallback. Atualize a lógica regenerada para preferir os campos novos, mas cair para os antigos quando o novo estiver ausente. Só depois de leituras estáveis, mude as escritas para os campos novos.
-
Limpeza por último. Uma vez com confiança (e um plano de rollback), remova campos antigos e aperte constraints: adicione NOT NULL, UNIQUE e FKs.
Exemplo concreto: você quer substituir um status texto livre por um status_id apontando para uma tabela statuses. Adicione status_id como nullable, faça backfill a partir dos valores de texto, atualize o app para ler status_id mas cair para status quando for null, e finalmente remova status e torne status_id obrigatório. Isso mantém cada regeneração segura porque o banco é compatível em cada estágio.
Padrões práticos que você pode reutilizar
Quando seu backend é regenerado, pequenos ajustes no esquema podem repercutir em APIs, validações e formulários. O objetivo da evolução segura é fazer mudanças de modo que dados antigos permaneçam válidos enquanto o novo código é distribuído.
Padrão 1: rename sem quebrar
Um rename direto é arriscado porque registros e código antigo ainda podem esperar o campo original. Uma abordagem mais segura é tratar um rename como um período curto de migração.
- Adicione a nova coluna (por exemplo,
customer_phone) e mantenha a antiga (phone). - Atualize a lógica para dual-write: ao salvar, escreva em ambos os campos.
- Backfill as linhas existentes para que
customer_phoneseja preenchida. - Altere leituras para a nova coluna quando a cobertura estiver elevada.
- Remova a coluna antiga em release posterior.
Isso funciona bem em ferramentas como AppMaster onde a regeneração reconstrói modelos de dados e endpoints a partir do esquema atual. Dual-write mantém as duas gerações de código felizes durante a transição.
Padrão 2: dividir um campo em dois
Dividir full_name em first_name e last_name é similar, mas o backfill é mais complicado. Mantenha full_name até ter certeza de que a divisão está completa.
Uma regra prática: não remova o campo original até que todo registro esteja backfilled ou tenha um fallback claro. Se o parsing falhar, armazene a string inteira em last_name e marque o registro para revisão.
Padrão 3: tornar um campo obrigatório
Transformar um campo nullable em obrigatório é um clássico que quebra produção. A ordem segura é: backfill primeiro, depois impor.
O backfill pode ser mecânico (definir um default) ou orientado pelo negócio (pedir aos usuários para completar). Só depois que os dados estiverem completos adicione NOT NULL e atualize validações. Se seu backend regenerado adiciona validações mais rígidas automaticamente, essa sequência evita falhas surpresa.
Padrão 4: mudar um enum com segurança
Mudanças em enums quebram quando código antigo envia valores antigos. Durante a transição, aceite ambos. Se você substituir "pending" por "queued", mantenha os dois valores válidos e mapeie-os na lógica. Quando confirmar que nenhum cliente envia o valor antigo, remova-o.
Se a mudança precisa sair em uma única release, reduza o risco limitando o blast radius:
- Adicione campos novos mas mantenha os antigos, mesmo que não usados.
- Use um default de banco para que inserts continuem funcionando.
- Torne o código tolerante: leia do novo e caia para o antigo.
- Adicione uma camada temporária de mapeamento (valor antigo recebido, novo valor armazenado).
Esses padrões mantêm migrações previsíveis mesmo quando o código regenerado muda comportamento rapidamente.
Erros comuns que causam surpresas
As maiores surpresas acontecem quando as pessoas tratam a regeneração do código como um botão mágico de reset. Backends regenerados mantêm seu código limpo, mas o banco de produção ainda contém os dados de ontem, com a forma de ontem. Evolução de esquema segura frente à regeneração significa planejar para ambos: o novo código que será gerado e os registros antigos que ainda estarão nas tabelas.
Uma armadilha comum é presumir que a plataforma “vai cuidar das migrações”. Por exemplo, no AppMaster você pode regenerar um backend em Go a partir do modelo do Data Designer, mas a plataforma não pode adivinhar como você quer transformar dados reais de clientes. Se você adicionar um campo obrigatório novo, ainda precisa de um plano claro para como as linhas existentes receberão valores.
Outra surpresa é dropar ou renomear campos cedo demais. Um campo pode parecer não usado na UI principal, mas ainda ser lido por um relatório, uma exportação agendada, um webhook ou uma tela administrativa que alguém abre raramente. A mudança parece segura em testes, então falha em produção porque um caminho de código esquecido ainda espera o nome antigo.
Aqui estão cinco erros que tendem a causar rollbacks noturnos:
- Mudar o esquema e regenerar código, mas nunca escrever ou verificar a migração de dados que torna linhas antigas válidas.
- Renomear ou deletar uma coluna antes de todos os leitores e escritores serem atualizados e deployados.
- Fazer backfill de uma tabela grande sem checar quanto tempo levará e se bloqueará outras escritas.
- Adicionar uma nova constraint (NOT NULL, UNIQUE, foreign key) primeiro e depois descobrir dados legados que a quebram.
- Esquecer jobs em background, exports e relatórios que ainda leem os campos antigos.
Um cenário simples: você renomeia phone para mobile_number, adiciona NOT NULL e regenera. As telas do app podem funcionar, mas uma exportação CSV antiga ainda seleciona phone, e milhares de registros têm mobile_number nulo. A correção geralmente é uma mudança em fases: adicione a nova coluna, escreva em ambos por um tempo, faça backfill com segurança, então aperte constraints e remova o campo antigo somente depois de provar que nada depende dele.
Checklist rápido pré-deploy para migrações mais seguras
Quando seu backend é regenerado, o código pode mudar rápido, mas seus dados de produção não perdoam surpresas. Antes de enviar uma mudança de esquema, faça uma checagem rápida “isso pode falhar com segurança?”. Isso torna a evolução de esquema segura e entediante (o que é bom).
As 5 checagens que pegam a maioria dos problemas
- Backfill: tamanho e velocidade — estime quantas linhas existentes precisam ser atualizadas (ou reescritas) e quanto tempo isso levará em produção. Um backfill que vai bem em um banco pequeno pode levar horas em dados reais e deixar o app lento.
- Locks e risco de downtime — identifique se a mudança pode bloquear escritas. Algumas operações (alterar tabelas grandes, mudar tipos) podem manter locks tempo suficiente para causar timeouts. Se houver chance de bloqueio, planeje um rollout mais seguro (adicionar coluna primeiro, backfill depois, trocar o código por último).
- Compatibilidade entre código antigo e novo esquema — assuma que o backend antigo pode rodar por um curto período contra o esquema novo durante deploy ou rollback. Pergunte: a versão anterior vai ler e escrever sem travar? Se não, você precisa de um release em dois passos.
- Defaults e comportamento de null — para colunas novas, decida o que acontece com registros existentes. Vão ser NULL ou precisam de default? Garanta que sua lógica trate valores ausentes como normais, especialmente flags, status e timestamps.
- Sinais de monitoramento para observar — escolha os alarmes exatos que você vai vigiar pós-deploy: taxa de erros (falhas de API), queries lentas no banco, falhas em filas/jobs e qualquer ação de usuário crítica (checkout, login, submit). Também monitore bugs silenciosos, como pico em erros de validação.
Um exemplo rápido
Se você adicionar um campo obrigatório status à tabela orders, não o imponha como NOT NULL sem default em um só passo. Primeiro adicione como nullable com um default para novas linhas, faça deploy do código regenerado que lida com status ausente, depois backfill as linhas antigas e só então aperte a constraint.
No AppMaster, essa mentalidade é especialmente útil porque o backend pode ser regenerado com frequência. Trate cada mudança de esquema como um pequeno release com rollback fácil, e suas migrações permanecem previsíveis.
Exemplo: evoluindo um app em produção sem quebrar registros existentes
Imagine uma ferramenta interna de suporte onde agentes marcam tickets com um campo de texto livre chamado priority (ex.: "high", "urgent", "HIGH", "p1"). Você quer mudar para um enum estrito para que relatórios e regras de roteamento parem de adivinhar.
A abordagem segura é uma mudança em duas releases que mantém registros antigos válidos enquanto o backend é regenerado.
Release 1: expandir, escrever em ambos e backfill
Comece expandindo o esquema sem remover nada. Adicione um novo campo enum, por exemplo priority_enum com valores como low, medium, high, urgent. Mantenha o campo original priority_text.
Depois, atualize a lógica para que tickets novos e editados escrevam em ambos os campos. Em uma ferramenta sem código como AppMaster, isso tipicamente significa ajustar o modelo no Data Designer e atualizar o Business Process para mapear a entrada para o enum e também armazenar o texto original.
Em seguida, faça backfill dos tickets existentes em pequenos lotes. Mapeie textos comuns para o enum (p1 e urgent → urgent, HIGH → high). Tudo desconhecido pode temporariamente mapear para medium enquanto você revisa.
O que os usuários veem: idealmente nada muda ainda. A UI pode continuar mostrando o mesmo controle de prioridade, mas nos bastidores você está populando o enum novo. Relatórios podem começar a usar o enum assim que o backfill estiver em andamento.
Release 2: contrair e remover a trilha antiga
Depois de estar confiante, mude leituras para usar apenas priority_enum, atualize filtros e dashboards e então remova priority_text em uma migração posterior.
Antes da Release 2, valide com uma amostra pequena para pegar casos de borda:
- Escolha 20 a 50 tickets de diferentes times e idades.
- Compare a prioridade exibida com o enum armazenado.
- Verifique contagens por valor do enum para identificar picos suspeitos (por exemplo, muitos
medium).
Se aparecerem problemas, o rollback é simples porque a Release 1 manteve o campo antigo: redeploy da Release 1 e faça a UI ler priority_text novamente enquanto corrige o mapeamento e relança o backfill.
Próximos passos: torne a evolução de esquema um hábito repetível
Se você quer migrações previsíveis, trate mudanças de esquema como um pequeno projeto, não como uma edição rápida. O objetivo é simples: cada mudança deve ser fácil de explicar, fácil de ensaiar e difícil de quebrar acidentalmente.
Um modelo visual de dados ajuda porque torna o impacto visível antes do deploy. Quando você vê tabelas, relações e tipos em um lugar, percebe coisas que um script pode ocultar, como um campo obrigatório sem default seguro ou uma relação que vai orfanizar registros antigos. Faça um rápido “quem depende disso?”: APIs, telas, relatórios e jobs em background.
Quando precisar mudar um campo já em uso, prefira um período curto de transição com campos duplicados. Por exemplo, adicione phone_e164 mantendo phone_raw por uma ou duas releases. Atualize a lógica para ler do novo quando presente e cair para o antigo quando não estiver. Escreva em ambos durante a transição e remova o antigo só depois de verificar que o backfill completou.
Disciplina de ambientes é o que transforma boas intenções em releases seguros. Mantenha dev, staging e produção alinhados, mas não os trate como idênticos.
- Dev: prove que o backend regenerado inicia limpo e fluxos básicos funcionam após regen.
- Staging: execute o plano de migração completo em dados parecidos com produção e verifique queries, relatórios e imports chave.
- Produção: faça deploy quando tiver plano de rollback, monitoramento claro e um conjunto pequeno de checagens “must pass”.
Faça do seu plano de migração um documento real, mesmo que curto. Inclua: o que muda, a ordem, como fazer backfill, como verificar e como voltar atrás. Então execute end-to-end em um ambiente de teste antes de tocar produção.
Se você usa AppMaster, apoie-se no Data Designer para raciocinar visualmente sobre o modelo e deixe a regeneração manter seu código consistente com o esquema atualizado. O hábito que torna tudo previsível é deixar as migrações explícitas: você pode iterar rápido, mas cada mudança tem um caminho planejado para os dados de produção existentes.


