Migrations de banco de dados: como versionar o schema sem quebrar produção
Entenda o que é uma migration, por que alterar uma tabela direto em produção é arriscado, e como estruturar migrations reversíveis e seguras pra mudanças de schema que não derrubam o sistema no meio do caminho.
Já mostrei aqui como criar SQL do zero e como fazer deploy em servidor, mas ficou faltando uma peça que conecta os dois: como o schema do banco muda ao longo do tempo, junto com o código, sem alguém precisar entrar direto no banco de produção rodando ALTER TABLE na mão e torcendo pra dar certo.
O problema de mexer direto no banco
Alterar uma tabela em produção sem nenhum controle parece inofensivo quando é uma mudança pequena, tipo adicionar uma coluna. O problema aparece na escala: cada ambiente (sua máquina, homologação, produção) pode acabar com um schema levemente diferente, porque alguém rodou um ALTER TABLE numa máquina e esqueceu de replicar em outra. Depois de algumas semanas, ninguém tem certeza de qual é o schema real de cada lugar, e um deploy que funciona local quebra em produção porque uma coluna que o código espera não existe lá.
O que é uma migration
Uma migration é um arquivo que descreve uma mudança específica de schema, versionado junto com o resto do código, no mesmo repositório Git. Em vez de alterar o banco na mão, você escreve o que precisa mudar, e uma ferramenta aplica essa mudança de forma controlada, registrando quais migrations já rodaram em cada ambiente.
// migrations/20260914120000_adiciona_telefone_usuarios.js
export async function up(db) {
await db.schema.alterTable("usuarios", (table) => {
table.string("telefone").nullable();
});
}
export async function down(db) {
await db.schema.alterTable("usuarios", (table) => {
table.dropColumn("telefone");
});
}O up descreve a mudança pra frente, o down descreve como desfazer ela. Esse par é o que torna a migration reversível: se algo der errado depois de aplicar, existe um caminho definido de volta, em vez de precisar reconstruir o schema anterior de memória.
Como isso resolve a divergência entre ambientes
Cada migration recebe um identificador único (geralmente um timestamp, como no exemplo acima) e a ferramenta de migration guarda, numa tabela própria dentro do banco, quais dessas migrations já foram aplicadas naquele ambiente específico:
SELECT * FROM migrations_executadas;
-- 20260901090000_cria_tabela_usuarios
-- 20260905143000_cria_tabela_pedidos
-- 20260914120000_adiciona_telefone_usuariosQuando você roda o comando de migration em qualquer ambiente, a ferramenta compara essa lista com os arquivos que existem no projeto, e aplica só os que ainda faltam, na ordem certa. Isso garante que produção, homologação e a máquina de cada desenvolvedor cheguem sempre no mesmo schema final, não importa a ordem em que cada pessoa rodou o comando.
Mudanças que quebram o sistema no meio do caminho
O detalhe mais delicado de migration em produção é: o deploy do código novo e a aplicação da migration não acontecem no mesmo instante exato, geralmente a migration roda antes do código novo subir. Isso significa que, por um período curto, o código antigo ainda está rodando contra um schema já alterado.
Renomear uma coluna direto é um exemplo clássico que quebra isso:
// perigoso: o código antigo ainda espera a coluna "nome"
export async function up(db) {
await db.schema.alterTable("usuarios", (table) => {
table.renameColumn("nome", "nome_completo");
});
}Assim que essa migration roda, qualquer instância do código antigo ainda em execução, que ainda consulta nome, começa a falhar, mesmo antes do deploy do código novo terminar. O caminho mais seguro é quebrar isso em etapas, cada uma compatível com o código antigo e o novo rodando ao mesmo tempo: adicionar a coluna nova, fazer o código escrever nas duas, migrar o dado existente, só depois de tudo estabilizado remover a coluna antiga numa migration separada, dias ou semanas depois.
Nunca editar uma migration que já rodou em produção
Uma vez que uma migration já foi aplicada em algum ambiente compartilhado, editar o arquivo dela depois é um erro comum que causa dor de cabeça. Bancos diferentes, em ambientes diferentes, já rodaram a versão antiga daquele arquivo e estão registrados como "já migrados", então a edição nunca chega até eles. Se algo precisa ser corrigido, o caminho certo é criar uma migration nova desfazendo ou ajustando o que a anterior fez, mantendo o histórico completo e verdadeiro do que realmente aconteceu com o schema, passo a passo.
Fechando
Migration não é burocracia extra, é o que transforma mudança de schema em algo rastreável, revisável em Pull Request como qualquer outro código, e reproduzível em qualquer ambiente na mesma ordem. O cuidado real está em pensar na mudança como um processo em etapas, compatível com o código antigo por um tempo, em vez de tratar o schema novo e o código novo como se sempre chegassem no ar exatamente no mesmo segundo.
Leia também