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:

  1. Um desenvolvedor gera uma nova migration via CLI:
# Usando Flyway
flyway migrate

# Usando Alembic
alembic revision --autogenerate -m "adicionar_tabela_pedidos"
  1. A migration é commitada no repositório Git.
  2. Outro desenvolvedor faz pull e executa as migrations pendentes.
  3. Em caso de conflito de schema (dois devs criam a mesma tabela), resolve-se via merge manual no Git, ajustando as migrations conflitantes.
  4. 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 TABLE desnecessá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:

  1. Migration não destrutiva: adiciona nova coluna.
  2. Aplicação passa a escrever em ambas as colunas (feature flag).
  3. 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.