Capítulo 08: Controle de Versão de Banco de Dados e Migrações em Go
Especialização em Backend com Go & Gin • Gin Framework & Go 1.22+ • GORM, Clean Architecture, Concorrência e Alta Performance
🗺️ Mapa Conceitual do Tópico
flowchart TD
A["Arquivos de Migração SQL (//go:embed migrations/*.sql)"] --> B["golang-migrate / goose Engine"]
C["Esquema Físico Atual do Banco de Dados"] --> D["Tabela de Controle: schema_migrations"]
B --> E{"Comparação de Versão Atual vs Disponível"}
D --> E
E -->|"Versão Banco < Versão Arquivos"| F["Execução Ordenada: 000001_up.sql -> 000002_up.sql"]
F --> G["Atualiza Versão em schema_migrations"]
G --> H["Banco Pronto: Inicia Servidor Gin (app.Run)"]
E -->|"Falha na Migração (Erro DDL)"| I["Estado 'Dirty' Registrado na Tabela"]
I --> J["Bloqueio Defensivo do Deploy (Abort Fatal)"]
subgraph ROLLBACK["Reversão em Caso de Incidente"]
K["migrate -path ./migrations -database ... down 1"]
K --> L["Executa 000002_down.sql e Restaura Versão Anterior"]
end
style A fill:#e1f5fe,stroke:#03a9f4,stroke-width:2px
style B fill:#fff3e0,stroke:#ff9800,stroke-width:2px
style C fill:#ede7f6,stroke:#7e57c2,stroke-width:2px
style D fill:#e8f5e9,stroke:#4caf50,stroke-width:2px
style E fill:#fce4ec,stroke:#e91e63,stroke-width:2px
style F fill:#e0f2f1,stroke:#009688,stroke-width:2px
style G fill:#c8e6c9,stroke:#2e7d32,stroke-width:2px
style H fill:#c8e6c9,stroke:#2e7d32,stroke-width:2px
style I fill:#ffebee,stroke:#e53935,stroke-width:2px
style J fill:#b71c1c,stroke:#b71c1c,stroke-width:2px,color:#fff
style K fill:#fffde7,stroke:#fbc02d,stroke-width:2px
🏛️ 1. Por que AutoMigrate é Inadequado para Produção
O método AutoMigrate do GORM é excelente para prototipagem rápida local, mas sua utilização em ambientes de homologação e produção é amplamente desaconselhada pela engenharia de software corporativa por quatro limitações fundamentais:
- Ausência de Reversibilidade (Rollback): O GORM não gera scripts reversos. Se um novo modelo causar problemas operacionais, não há mecanismo para desfazer a alteração no banco de dados automaticamente.
- Incapacidade de Renomear Colunas com Segurança: Renomear um campo na struct de
TelefoneparaCelularfaz com que oAutoMigratecrie uma nova colunacelularmantendo a antigatelefoneintacta, sem migrar os dados legados. - Impossibilidade de Remover Colunas (No-Drop): Para evitar desastres, o GORM nunca executa
DROP COLUMN. Ao longo de meses, o banco acumula dezenas de colunas mortas e índices obsoletos. - Falta de Histórico e Auditoria: Não há registro determinístico de quais alterações DDL foram aplicadas em cada versão da aplicação, quebrando requisitos de compliance (SOC 2, ISO 27001).
⚙️ 2. O Padrão Canônico: golang-migrate
A biblioteca e CLI mais utilizada no ecossistema Go corporativo é o golang-migrate/migrate. Ela adota a convenção de pares simétricos de arquivos SQL versionados por timestamp ou números sequenciais:
migrations/
├── 000001_criar_tabela_usuarios.up.sql
├── 000001_criar_tabela_usuarios.down.sql
├── 000002_adicionar_documento_usuarios.up.sql
└── 000002_adicionar_documento_usuarios.down.sql
Exemplo de Scripts DDL Simétricos
-- migrations/000001_criar_tabela_usuarios.up.sql
CREATE TABLE IF NOT EXISTS usuarios (
id BIGSERIAL PRIMARY KEY,
nome VARCHAR(120) NOT NULL,
email VARCHAR(120) UNIQUE NOT NULL,
criado_em TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_usuarios_email ON usuarios(email);
-- migrations/000001_criar_tabela_usuarios.down.sql
DROP INDEX IF EXISTS idx_usuarios_email;
DROP TABLE IF EXISTS usuarios;
💻 3. Execução Programática de Migrações com //go:embed
A partir do Go 1.16+, é possível embutir toda a pasta de arquivos SQL diretamente dentro do binário final compilado da aplicação utilizando a diretiva //go:embed. Isso elimina a necessidade de copiar pastas de scripts SQL para dentro do contêiner Docker de produção:
package database
import (
"embed"
"errors"
"fmt"
"log"
"github.com/golang-migrate/migrate/v4"
_ "github.com/golang-migrate/migrate/v4/database/postgres"
"github.com/golang-migrate/migrate/v4/source/iofs"
)
//go:embed migrations/*.sql
var migrationsFS embed.FS
// ExecutarMigracoes aplica todos os scripts pendentes até a versão mais recente
func ExecutarMigracoes(databaseURL string) error {
driverFonte, err := iofs.New(migrationsFS, "migrations")
if err != nil {
return fmt.Errorf("falha ao carregar sistema de arquivos embutido: %w", err)
}
m, err := migrate.NewWithSourceInstance("iofs", driverFonte, databaseURL)
if err != nil {
return fmt.Errorf("falha ao instanciar migrador: %w", err)
}
defer m.Close()
if err := m.Up(); err != nil && !errors.Is(err, migrate.ErrNoChange) {
return fmt.Errorf("erro durante a execução das migrações: %w", err)
}
log.Println("[MIGRAÇÕES] Esquema de banco de dados atualizado com sucesso (Head Version).")
return nil
}
🛡️ 4. O Padrão Zero-Downtime no PostgreSQL
Em bancos de dados em produção sob tráfego contínuo de milhares de requisições por segundo, comandos DDL descuidados podem bloquear tabelas inteiras (Exclusive Lock), derrubando a API:
1. Criação Segura de Índices com CONCURRENTLY
No PostgreSQL, criar um índice comum bloqueia operações de escrita na tabela enquanto o índice é computado. A diretiva CONCURRENTLY constrói o índice em segundo plano sem bloquear INSERTs ou UPDATEs:
-- ⚠️ NUNCA RODE EM UMA TRANSAÇÃO ATÔMICA (PostgreSQL proíbe CONCURRENTLY dentro de transação)
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_pedidos_cliente_id ON pedidos(cliente_id);
2. Adição Segura de Colunas NOT NULL (Padrão Expand & Contract)
- Passo 1 (Migração N): Adiciona a nova coluna permitindo
NULL. - Passo 2 (Backfill): Aplica UPDATE nos registros existentes com valor padrão via worker ou lote.
- Passo 3 (Migração N+1): Aplica a constraint
ALTER TABLE usuarios ALTER COLUMN status SET NOT NULL.