Como Reverter Migrations com Segurança Sem Perder Dados em Produção
O problema real: migrations não são simétricas
Uma migration que adiciona uma coluna é trivial de reverter: basta dropar a coluna. Uma migration que dropa uma coluna, renomeia uma tabela ou altera o tipo de um campo não tem caminho de volta automático. Os dados que existiam antes da migration desaparecem no momento em que ela roda.
A maioria dos ORMs gera o método down() como espelho do up(), e isso cria uma falsa sensação de segurança. O down() reverte a estrutura do schema, não os dados. Se você dropou a coluna legacy_email e precisa reverter, o down() recria a coluna vazia. Os 200 mil emails que estavam lá sumiram.
Esse post trata de como planejar migrations que permitem rollback seguro, como executar reversões quando o plano falhou, e quando a resposta correta é não reverter.
Se você ainda não tem um fluxo maduro de migrations, vale ler o post sobre versionamento de schema com TypeORM e o guia de migrations seguras com Prisma antes de continuar.
Classificação de risco: nem toda migration precisa do mesmo cuidado
Antes de decidir a estratégia de rollback, classifique a migration:
| Tipo de operação | Risco de perda de dados | Reversível automaticamente? | Estratégia recomendada |
|---|---|---|---|
| Adicionar coluna nullable | Nenhum | Sim (drop column) | Rollback direto |
| Adicionar índice | Nenhum | Sim (drop index) | Rollback direto |
| Remover coluna | Alto | Não (dados perdidos) | Backup da coluna antes |
| Renomear coluna/tabela | Médio | Parcial (renomear de volta) | Deploy em fases |
| Alterar tipo de coluna | Alto | Parcial (cast pode falhar) | Coluna shadow + backfill |
| Dropar tabela | Crítico | Não | Dump da tabela antes |
| Migrar dados entre tabelas | Alto | Não (depende da lógica) | Migration reversível com tabela auxiliar |
A regra é: se a migration destrói informação, o rollback precisa de uma etapa extra antes da execução. Não depois.
A técnica de coluna shadow para alterações destrutivas
Renomear uma coluna ou alterar seu tipo parece simples. Na prática, qualquer deploy que muda o nome de uma coluna quebra queries da versão anterior da aplicação que ainda estão rodando. Se você usa estratégias de deploy como blue-green ou rolling updates, a versão antiga e a nova coexistem por alguns segundos ou minutos.
A solução é o deploy em três fases com coluna shadow:
Fase 1: adicionar a coluna nova sem remover a antiga
-- Fase 1: cria a nova coluna e copia os dados existentes
-- POR QUE nullable: a aplicação antiga ainda escreve na coluna original,
-- e a nova coluna não pode ter constraint NOT NULL antes do backfill completar
ALTER TABLE users ADD COLUMN display_name VARCHAR(255);
UPDATE users SET display_name = username WHERE display_name IS NULL;
Fase 2: aplicação escreve nas duas colunas (dual-write)
// src/repositories/user.repository.ts
import { PrismaClient } from "@prisma/client";
const prisma = new PrismaClient();
async function updateUsername(userId: string, newName: string) {
// POR QUE dual-write: garante que ambas as colunas ficam sincronizadas
// durante o período de transição entre deploys
await prisma.user.update({
where: { id: userId },
data: {
username: newName,
displayName: newName,
},
});
}
Fase 3: remover a coluna antiga (só depois que nenhuma versão antiga está rodando)
-- Fase 3: só execute quando TODAS as instâncias rodam a versão nova
-- POR QUE esperar: se uma instância antiga ainda lê "username",
-- dropar a coluna causa erro 500 nessa instância
ALTER TABLE users DROP COLUMN username;
Se algo der errado na fase 2 ou 3, o rollback é trivial: a coluna original ainda existe com os dados intactos. Você reverte o deploy da aplicação e a coluna shadow fica órfã até o próximo ciclo
Leia o artigo completo em https://www.vivodecodigo.com.br/backend/reverter-migrations-seguranca-sem-perder-dados-producao