Backend - Migrações de Banco de Dados¶
Este documento descreve o processo de migrações de banco de dados, incluindo auto-migrate do GORM e migrações SQL manuais.
Índice¶
- Visão Geral
- Auto-Migrate (GORM)
- Migrações SQL Manuais
- Processo de Criação de Migrações
- Executando Migrações
- Rollback
Visão Geral¶
O sistema usa uma abordagem híbrida para migrações:
- Auto-Migrate (GORM): Cria tabelas e adiciona colunas automaticamente
- Migrações SQL Manuais: Para alterações de tipo, constraints complexas e dados
Localização: migrations/
ORM: GORM v1.25.5
Banco: PostgreSQL 12+
Atualização importante (marketplace de cursos)¶
No ciclo de evolução do marketplace de cursos foram adicionadas as migrações:
042_marketplace_course_domain_refactor.sql043_marketplace_revenue_share_bundle_recommendation.sql044_marketplace_catalog_and_review_moderation.sql045_add_marketplace_course_permission_modules.sql046_marketplace_course_logical_delete.sql047_add_client_method_onboarding_fields.sql
Detalhes funcionais em:
Atualização importante (onboarding cliente + PBI)¶
A migração 047_add_client_method_onboarding_fields.sql adiciona suporte ao onboarding obrigatório de método no perfil da cliente:
knows_billings_methodknows_own_pbiwants_professional_guidancemethod_onboarding_version(default0)pbi_defined_by(client|professional)
Estratégia de compatibilidade:
- Perfis existentes são migrados para
method_onboarding_version = 1(legadas). - Perfis já com PBI preenchido recebem
pbi_defined_by = professional. - Novas clientes começam em
method_onboarding_version = 0e só concluem comversion = 2.
Auto-Migrate (GORM)¶
Funcionalidade¶
GORM AutoMigrate cria/atualiza tabelas automaticamente baseado nos modelos.
Localização: internal/database/database.go
Execução: Automática na inicialização do banco
O que AutoMigrate Faz¶
✅ Cria: - Tabelas que não existem - Colunas que não existem - Índices que não existem - Foreign keys que não existem
✅ Atualiza: - Adiciona novas colunas - Adiciona novos índices
❌ NÃO Faz: - Alterar tipos de colunas existentes - Remover colunas (soft delete apenas) - Alterar constraints existentes - Migrar dados
Modelos Migrados Automaticamente¶
DB.AutoMigrate(
&models.User{},
&models.Account{},
&models.Plan{},
&models.Subscription{},
&models.Cycle{},
&models.Observation{},
&models.SyncLog{},
&models.Payment{},
&models.ProfessionalPatient{},
&models.ProfessionalProfile{},
&models.ClientProfile{},
&models.Message{},
&models.PasswordReset{},
&models.EmailVerification{},
&models.Appointment{},
&models.ProfessionalBankAccount{},
&models.Course{},
&models.AvailabilitySlot{},
&models.WorkSchedule{},
&models.Sensation{},
&models.Appearance{},
&models.Symbol{},
&models.DayBasedRule{},
&models.Settings{},
&models.CycleComment{},
&models.UserAuthProvider{},
&models.Notification{},
)
Limitações¶
AutoMigrate não altera tipos:
// Se mudar de string para int no modelo, AutoMigrate NÃO altera
// Precisa de migração SQL manual
AutoMigrate não remove colunas:
// Se remover campo do modelo, coluna permanece no banco
// Precisa de migração SQL manual para remover
Migrações SQL Manuais¶
Quando Usar¶
Migrações SQL manuais são necessárias para:
- Alterar tipos de colunas:
VARCHAR→TEXTTEXT→JSONB-
TIMESTAMP→DATE -
Adicionar constraints complexas:
- Unique constraints condicionais
- Check constraints
-
Foreign keys com regras específicas
-
Migrar dados:
- Transformar dados existentes
- Popular campos calculados
-
Normalizar dados
-
Remover colunas:
- Quando campo é removido do modelo
Estrutura de Migrações¶
Localização: migrations/
Nomenclatura:
{numero}_{descricao}.sql
Exemplos:
- 001_add_symbol_id_and_appearance_id_to_observations.sql
- 002_change_day_based_rule_to_jsonb.sql
- 017_migrate_username_to_email_login.sql
Características¶
Idempotência: Migrações devem ser idempotentes (podem ser executadas múltiplas vezes):
-- ✅ BOM - Verifica se coluna já existe
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_name = 'observations' AND column_name = 'symbol_id'
) THEN
ALTER TABLE observations ADD COLUMN symbol_id INTEGER;
END IF;
END $$;
Verificações Condicionais:
-- Verificar se constraint já existe antes de criar
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM pg_constraint WHERE conname = 'unique_username'
) THEN
ALTER TABLE users ADD CONSTRAINT unique_username UNIQUE (username);
END IF;
END $$;
Processo de Criação de Migrações¶
1. Identificar Necessidade¶
Quando criar migração: - Alterar tipo de coluna no modelo - Adicionar constraint complexa - Migrar dados existentes - Remover coluna
2. Criar Arquivo SQL¶
Nome: {proximo_numero}_{descricao}.sql
Exemplo:
-- 022_add_new_field_to_observations.sql
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_name = 'observations' AND column_name = 'new_field'
) THEN
ALTER TABLE observations ADD COLUMN new_field VARCHAR(255);
END IF;
END $$;
3. Testar Migração¶
Em desenvolvimento:
# Backup do banco
pg_dump -h localhost -U postgres billings_ease > backup.sql
# Executar migração
psql -h localhost -U postgres -d billings_ease -f migrations/022_add_new_field_to_observations.sql
# Verificar resultado
psql -h localhost -U postgres -d billings_ease -c "\d observations"
4. Criar Rollback (Opcional)¶
Localização: migrations/rollback/
Nome: {numero}_rollback_{descricao}.sql
Exemplo:
-- rollback/022_rollback_new_field.sql
DO $$
BEGIN
IF EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_name = 'observations' AND column_name = 'new_field'
) THEN
ALTER TABLE observations DROP COLUMN new_field;
END IF;
END $$;
5. Documentar¶
Adicionar descrição no README.md das migrações:
## Migrações
- 022: Adiciona campo `new_field` à tabela `observations`
Executando Migrações¶
Opção 1: Via psql (Linha de Comando)¶
# Conectar ao banco
psql -h $DB_HOST -U $DB_USER -d $DB_NAME
# Executar migração
\i migrations/022_add_new_field_to_observations.sql
Ou diretamente:
psql -h $DB_HOST -U $DB_USER -d $DB_NAME -f migrations/022_add_new_field_to_observations.sql
Opção 2: Via Script Go¶
Localização: cmd/migrate/main.go
Executar:
go run ./cmd/migrate
Funcionalidade: - Executa todas as migrações em ordem numérica - Verifica se já foram executadas - Log de progresso
Opção 3: Via Ferramenta de Migrations¶
golang-migrate:
migrate -path migrations -database "postgres://user:password@host:port/dbname?sslmode=disable" up
flyway:
flyway migrate -url=jdbc:postgresql://host:port/dbname -user=user -password=password
Ordem de Execução¶
Migrações devem ser executadas em ordem numérica:
001 → 002 → 003 → ... → 022
Importante: Não pular números. Se criar migração 022, próxima deve ser 023.
Migrações Existentes¶
Lista de Migrações¶
- 001 - Adiciona
symbol_ideappearance_idà tabelaobservations - 002 - Converte campos de
day_based_rulede TEXT para JSONB - 003 - Cria tabela
email_verifications - 004 - Adiciona
last_resend_atàemail_verifications - 005 - Adiciona
image_pathà tabelasymbol - 006 - Adiciona
relation_image_pathà tabelasymbol - 006 - Cria tabela
settings - 007 - Adiciona
had_intercourseà tabelaobservations - 008 - Remove colunas antigas de
symbol - 009 - Adiciona
is_first_day_of_menstruationà tabelaobservations - 010 - Garante que datas de ciclo sejam tipo DATE
- 011 - Garante que todas as datas civis sejam tipo DATE
- 012 - Adiciona
professional_notesà tabelaobservations - 013 - Cria tabela
cycle_comments - 014 - Adiciona
statusà tabelaprofessional_patients - 015 - Migra URLs do R2 para keys
- 016 - Adiciona campos PBI à
client_profiles - 017 - Migra login de username para email
- 018 - Cria tabela
user_auth_providers - 019 - Cria tabela
notifications - 020 - Atualiza paths de imagens de símbolos para SVG
- 021 - Normaliza paths de símbolos para filename
Migrações Importantes¶
017 - Migração de Username para Email¶
Objetivo: Sistema agora usa email como método de login principal.
Mudanças:
- username torna-se opcional
- Email passa a ser obrigatório e único
- Índice único condicional em username (permite múltiplos NULLs)
015 - Migração R2 URLs para Keys¶
Objetivo: Migrar de URLs completas para keys (paths relativos).
Processo:
- Extrai key de URLs existentes
- Atualiza campos *_key nos modelos
- Remove URLs antigas
010/011 - Migração de Datas¶
Objetivo: Garantir que todas as datas civis sejam tipo DATE (não TIMESTAMP).
Mudanças: - Converte colunas de TIMESTAMP para DATE - Preserva dados existentes - Aplica a modelos: Cycle, Observation, ClientProfile, etc.
Rollback¶
Quando Fazer Rollback¶
- Migração causou problemas
- Necessidade de reverter mudanças
- Testes de migração
Processo¶
1. Backup:
pg_dump -h $DB_HOST -U $DB_USER -d $DB_NAME > backup_before_rollback.sql
2. Executar Rollback:
psql -h $DB_HOST -U $DB_USER -d $DB_NAME -f migrations/rollback/022_rollback_new_field.sql
3. Verificar:
psql -h $DB_HOST -U $DB_USER -d $DB_NAME -c "\d observations"
Ordem de Rollback¶
Rollbacks devem ser executados na ordem inversa:
022 → 021 → 020 → ... → 001
Atenção¶
⚠️ Rollbacks podem causar perda de dados!
- Sempre fazer backup antes
- Verificar se rollback é seguro
- Algumas migrações não têm rollback (migrações de dados)
Boas Práticas¶
1. Idempotência¶
Sempre tornar migrações idempotentes:
-- ✅ BOM
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_name = 'table' AND column_name = 'column'
) THEN
ALTER TABLE table ADD COLUMN column VARCHAR(255);
END IF;
END $$;
-- ❌ EVITAR
ALTER TABLE table ADD COLUMN column VARCHAR(255); -- Falha se já existe
2. Transações¶
Usar transações quando possível:
BEGIN;
-- Operações
COMMIT;
Nota: Algumas operações DDL (ALTER TABLE) não podem ser revertidas em transação no PostgreSQL.
3. Backup Antes de Migrações¶
Sempre fazer backup antes de executar migrações em produção:
pg_dump -h $DB_HOST -U $DB_USER -d $DB_NAME > backup_$(date +%Y%m%d_%H%M%S).sql
4. Testar em Desenvolvimento Primeiro¶
- Testar migração em banco de desenvolvimento
- Verificar dados após migração
- Testar rollback (se aplicável)
- Aplicar em produção
5. Documentar Mudanças¶
Adicionar comentários explicativos nas migrações:
-- Migração 022: Adiciona campo new_field
-- Motivo: Necessário para nova funcionalidade X
-- Data: 2024-01-15
-- Autor: Nome
DO $$
BEGIN
-- Verificação e alteração
END $$;
6. Versionamento¶
Manter histórico de migrações:
- Commitar migrações no Git
- Nunca modificar migrações já executadas
- Criar nova migração para correções
7. Migrações de Dados¶
Para migrações que alteram dados:
-- Migrar dados existentes
UPDATE observations
SET new_field = COALESCE(old_field, 'default_value')
WHERE new_field IS NULL;
Exemplos de Migrações¶
Adicionar Coluna¶
-- 022_add_new_field.sql
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_name = 'observations' AND column_name = 'new_field'
) THEN
ALTER TABLE observations
ADD COLUMN new_field VARCHAR(255) DEFAULT '';
END IF;
END $$;
Alterar Tipo de Coluna¶
-- 023_change_field_type.sql
DO $$
BEGIN
IF EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_name = 'table'
AND column_name = 'field'
AND data_type = 'text'
) THEN
-- Converter dados primeiro
ALTER TABLE table
ALTER COLUMN field TYPE INTEGER USING field::INTEGER;
END IF;
END $$;
Adicionar Constraint¶
-- 024_add_unique_constraint.sql
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM pg_constraint
WHERE conname = 'unique_email'
) THEN
ALTER TABLE users
ADD CONSTRAINT unique_email UNIQUE (email);
END IF;
END $$;
Migrar Dados¶
-- 025_migrate_data.sql
-- Migrar dados de uma coluna para outra
UPDATE observations
SET new_field = old_field
WHERE new_field IS NULL AND old_field IS NOT NULL;
Remover Coluna¶
-- 026_remove_old_field.sql
DO $$
BEGIN
IF EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_name = 'table' AND column_name = 'old_field'
) THEN
ALTER TABLE table DROP COLUMN old_field;
END IF;
END $$;
Troubleshooting¶
Problema: Migração Falha¶
Causas comuns: - Coluna já existe - Constraint já existe - Tipo incompatível - Dados inválidos
Solução: - Verificar estado atual do banco - Adicionar verificações condicionais - Fazer backup antes
Problema: AutoMigrate Não Cria Tabela¶
Causas: - Modelo não está na lista de AutoMigrate - Erro de conexão - Permissões insuficientes
Solução:
- Verificar se modelo está em AutoMigrate()
- Verificar logs de erro
- Verificar permissões do usuário do banco
Problema: Migração de Tipo Falha¶
Causas: - Dados existentes incompatíveis com novo tipo - Constraint violada
Solução:
- Limpar dados inválidos primeiro
- Converter dados antes de alterar tipo
- Usar USING clause no ALTER TABLE