Ir para o conteúdo

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

  1. Visão Geral
  2. Auto-Migrate (GORM)
  3. Migrações SQL Manuais
  4. Processo de Criação de Migrações
  5. Executando Migrações
  6. Rollback

Visão Geral

O sistema usa uma abordagem híbrida para migrações:

  1. Auto-Migrate (GORM): Cria tabelas e adiciona colunas automaticamente
  2. 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.sql
  • 043_marketplace_revenue_share_bundle_recommendation.sql
  • 044_marketplace_catalog_and_review_moderation.sql
  • 045_add_marketplace_course_permission_modules.sql
  • 046_marketplace_course_logical_delete.sql
  • 047_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_method
  • knows_own_pbi
  • wants_professional_guidance
  • method_onboarding_version (default 0)
  • 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 = 0 e só concluem com version = 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:

  1. Alterar tipos de colunas:
  2. VARCHARTEXT
  3. TEXTJSONB
  4. TIMESTAMPDATE

  5. Adicionar constraints complexas:

  6. Unique constraints condicionais
  7. Check constraints
  8. Foreign keys com regras específicas

  9. Migrar dados:

  10. Transformar dados existentes
  11. Popular campos calculados
  12. Normalizar dados

  13. Remover colunas:

  14. 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

  1. 001 - Adiciona symbol_id e appearance_id à tabela observations
  2. 002 - Converte campos de day_based_rule de TEXT para JSONB
  3. 003 - Cria tabela email_verifications
  4. 004 - Adiciona last_resend_at à email_verifications
  5. 005 - Adiciona image_path à tabela symbol
  6. 006 - Adiciona relation_image_path à tabela symbol
  7. 006 - Cria tabela settings
  8. 007 - Adiciona had_intercourse à tabela observations
  9. 008 - Remove colunas antigas de symbol
  10. 009 - Adiciona is_first_day_of_menstruation à tabela observations
  11. 010 - Garante que datas de ciclo sejam tipo DATE
  12. 011 - Garante que todas as datas civis sejam tipo DATE
  13. 012 - Adiciona professional_notes à tabela observations
  14. 013 - Cria tabela cycle_comments
  15. 014 - Adiciona status à tabela professional_patients
  16. 015 - Migra URLs do R2 para keys
  17. 016 - Adiciona campos PBI à client_profiles
  18. 017 - Migra login de username para email
  19. 018 - Cria tabela user_auth_providers
  20. 019 - Cria tabela notifications
  21. 020 - Atualiza paths de imagens de símbolos para SVG
  22. 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

  1. Testar migração em banco de desenvolvimento
  2. Verificar dados após migração
  3. Testar rollback (se aplicável)
  4. 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


Referências