Database migrations: versionamento de schema
1. Por que versionar o schema do banco de dados?
Gerenciar mudanças no schema de um banco de dados manualmente é uma receita para desastres em produção. Quando times diferentes trabalham em paralelo, cada um alterando tabelas, colunas ou índices, a sincronização entre ambientes de desenvolvimento, staging e produção torna-se caótica. Um desenvolvedor adiciona uma coluna em sua máquina local, outro a remove em staging, e a produção fica com uma versão desatualizada — o resultado são erros em runtime e horas de debugging.
A evolução manual do schema, onde alguém executa scripts SQL avulsos diretamente no banco, não oferece rastreabilidade. Não há registro de quem fez o quê, quando, nem como reverter a mudança. Já a evolução automatizada, via migrations, resolve esses problemas:
- Rastreabilidade: cada migration é um arquivo versionado que documenta a mudança.
- Rollback: migrations possuem instruções de reversão (
down), permitindo desfazer alterações de forma segura. - Reprodutibilidade: qualquer ambiente pode ser reconstruído executando as migrations na ordem correta.
- Integração contínua: migrations podem ser validadas e executadas automaticamente em pipelines de CI/CD.
2. Conceitos fundamentais de migrations
Uma migration é um arquivo contendo mudanças incrementais no schema do banco de dados. Cada migration possui duas partes:
- Up: as instruções SQL que aplicam a mudança (ex:
CREATE TABLE,ALTER TABLE ADD COLUMN). - Down: as instruções que revertem a mudança (ex:
DROP TABLE,ALTER TABLE DROP COLUMN).
A estrutura típica de nomenclatura usa um timestamp seguido de uma descrição:
20250401120000_criar_tabela_usuarios.sql
20250401123000_adicionar_coluna_email.sql
20250401130000_criar_indice_email.sql
O banco de dados mantém uma tabela de controle, geralmente chamada schema_migrations ou _migrations_history. Essa tabela registra cada migration executada, impedindo que o mesmo arquivo seja aplicado duas vezes:
-- Exemplo de estrutura da tabela schema_migrations
CREATE TABLE schema_migrations (
version VARCHAR(255) PRIMARY KEY,
applied_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Após executar uma migration, insere-se o registro
INSERT INTO schema_migrations (version) VALUES ('20250401120000');
3. Estratégias de escrita de migrations
Migrations atômicas
Cada migration deve conter uma única mudança lógica. Isso facilita o entendimento, o review e o rollback. Exemplo de migration atômica:
-- UP: 20250401120000_criar_tabela_usuarios.sql
CREATE TABLE usuarios (
id SERIAL PRIMARY KEY,
nome VARCHAR(100) NOT NULL,
email VARCHAR(255) UNIQUE NOT NULL,
criado_em TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- DOWN:
DROP TABLE IF EXISTS usuarios;
Migrations reversíveis
Sempre implemente o down. Rollback seguro é um dos maiores benefícios das migrations. Exemplo de alteração reversível:
-- UP: 20250401123000_adicionar_coluna_idade.sql
ALTER TABLE usuarios ADD COLUMN idade INTEGER;
-- DOWN:
ALTER TABLE usuarios DROP COLUMN idade;
Migrations destrutivas vs. não destrutivas
Migrations que removem dados (ex: DROP COLUMN, DROP TABLE) são destrutivas. Use-as com cuidado, especialmente em produção. Prefira abordagens não destrutivas quando possível:
-- Destrutiva (cuidado!)
ALTER TABLE usuarios DROP COLUMN idade;
-- Não destrutiva (primeiro marcar como obsoleta)
ALTER TABLE usuarios ADD COLUMN idade_obsoleta BOOLEAN DEFAULT FALSE;
4. Boas práticas de nomenclatura e organização
O padrão YYYYMMDDHHMMSS_descricao.sql garante ordenação natural quando os arquivos são listados em ordem alfabética. Isso é essencial para que as migrations sejam executadas na sequência correta.
Organize as migrations em pastas:
migrations/
├── 20250401120000_criar_tabela_usuarios.sql
├── 20250401123000_adicionar_coluna_email.sql
└── 20250401130000_criar_indice_email.sql
O versionamento do banco de dados é independente do versionamento da aplicação. Enquanto a aplicação usa versionamento semântico (ex: v1.2.3), o banco usa a sequência de migrations. Uma aplicação v2.0 pode exigir que as migrations até 20250401130000 estejam aplicadas, mas não há relação direta com o número da versão.
5. Ferramentas e frameworks comuns
Ferramentas standalone
- Flyway: baseada em SQL puro, suporta múltiplos bancos. Exemplo de configuração:
# flyway.conf
flyway.url=jdbc:postgresql://localhost:5432/meubanco
flyway.user=admin
flyway.password=secreta
flyway.locations=filesystem:./migrations
- Liquibase: suporta formatos XML, YAML, JSON e SQL. Oferece rollback automático.
- Sqitch: foco em SQL puro com suporte a tags e dependências entre migrations.
Frameworks ORM com migrations embutidas
- Alembic (Python/SQLAlchemy): gera migrations automaticamente a partir de modelos.
- ActiveRecord Migrations (Ruby on Rails): migrations em Ruby com DSL própria.
- EF Core Migrations (C#/.NET): migrations em C# com suporte a snapshots.
A escolha entre SQL puro (Flyway, Sqitch) e DSL (Alembic, ActiveRecord) depende do time: SQL puro é mais portável e transparente, enquanto DSLs oferecem automação e integração com o ORM.
6. Fluxo de trabalho com migrations em equipe
O fluxo típico:
- Um desenvolvedor gera uma nova migration via CLI:
# Usando Flyway
flyway migrate
# Usando Alembic
alembic revision --autogenerate -m "adicionar_tabela_pedidos"
- A migration é commitada no repositório Git.
- Outro desenvolvedor faz pull e executa as migrations pendentes.
- Em caso de conflito de schema (dois devs criam a mesma tabela), resolve-se via merge manual no Git, ajustando as migrations conflitantes.
- No pipeline de CI/CD, as migrations são validadas (sintaxe SQL) e executadas em ordem antes dos testes.
7. Estratégias de deploy e rollback
Execução segura em produção
- Execute migrations dentro de transações (quando o banco suporta DDL transacional).
- Use janelas de manutenção para migrations que causam locking prolongado.
- Evite
LOCK TABLEdesnecessário.
Rollback reversível
Sempre teste o rollback em staging antes de aplicar em produção:
-- UP: adiciona coluna
ALTER TABLE usuarios ADD COLUMN telefone VARCHAR(20);
-- DOWN: remove coluna
ALTER TABLE usuarios DROP COLUMN telefone;
Blue/green e feature flags
Para mudanças destrutivas (ex: renomear coluna), use uma abordagem em fases:
- Migration não destrutiva: adiciona nova coluna.
- Aplicação passa a escrever em ambas as colunas (feature flag).
- Migration destrutiva: remove coluna antiga (após validação).
8. Testando migrations e integridade do schema
Verificação de idempotência
Uma migration bem escrita pode ser executada múltiplas vezes sem causar erros. Use IF NOT EXISTS ou IF EXISTS:
CREATE TABLE IF NOT EXISTS usuarios (
id SERIAL PRIMARY KEY,
nome VARCHAR(100)
);
Testes de rollback
Crie testes automatizados que:
1. Aplicam a migration (up).
2. Verificam o schema esperado.
3. Revertem a migration (down).
4. Verificam que o schema retornou ao estado anterior.
Snapshots de schema
Ferramentas como pg_dump --schema-only (PostgreSQL) ou mysqldump --no-data (MySQL) geram snapshots do schema. Compare snapshots entre ambientes para detectar divergências:
# Gerar snapshot de produção
pg_dump --schema-only -h producao -U admin meubanco > schema_producao.sql
# Gerar snapshot de staging
pg_dump --schema-only -h staging -U admin meubanco > schema_staging.sql
# Comparar
diff schema_producao.sql schema_staging.sql
Referências
- Flyway Documentation — Documentação oficial do Flyway, com guias de migrations baseadas em SQL puro e exemplos práticos.
- Liquibase Documentation — Documentação oficial do Liquibase, abordando formatos de migration e rollback automático.
- Alembic Tutorial — Tutorial oficial do Alembic para Python, incluindo geração automática de migrations e gerenciamento de versões.
- Active Record Migrations Guide — Guia oficial do Ruby on Rails sobre migrations, com exemplos de criação, rollback e boas práticas.
- Sqitch Documentation — Documentação do Sqitch, ferramenta de migrations focada em SQL puro com suporte a tags e dependências.