Sobre nós Guias Projetos Contactos
Админка
please wait

Declaração do Problema

Precisa de evoluir o esquema da sua base de dados ao longo do tempo, mantendo zero downtime, garantindo a integridade dos dados e suportando capacidades de rollback.

Princípios de Migração

PrincípioDescrição
Backward CompatibleA versão antiga da aplicação funciona com o novo esquema
Forward CompatibleA nova versão da aplicação funciona com o esquema antigo (durante o rollout)
ReversibleCada migração tem um script de rollback
IdempotentExecutar a migração várias vezes produz o mesmo resultado
Small & IncrementalPreferir muitas migrações pequenas a alterações «big-bang»

Padrões de Migração Seguros

1. Adicionar uma Coluna (Seguro)

-- Migração: add_email_verified_column
-- Seguro: nova coluna com valor por defeito; ainda não há leituras que dependam dela
ALTER TABLE users ADD COLUMN email_verified BOOLEAN DEFAULT false;
-- O código da aplicação pode ser feito deploy antes ou depois
-- O código antigo ignora a nova coluna; o novo código utiliza-a

2. Renomear uma Coluna (Multi-Etapas)

Passo 1: Adicionar nova coluna

-- Migração 001: add_full_name_column
ALTER TABLE users ADD COLUMN full_name VARCHAR(255);

Passo 2: Preencher dados (backfill)

-- Migração 002: backfill_full_name
UPDATE users SET full_name = CONCAT(first_name, ' ', last_name) WHERE full_name IS NULL;

Passo 3: Fazer deploy de código que escreve em ambas e lê da nova

// Versão 2 do código: escrita dupla
class User extends Model
{
protected $fillable = ['first_name', 'last_name', 'full_name'];
public function save(array $options = [])
{
// Escrever em ambas durante a transição
if ($this->isDirty('first_name') || $this->isDirty('last_name')) {
$this->full_name = $this->first_name . ' ' . $this->last_name;
}
return parent::save($options);
}
public function getDisplayNameAttribute()
{
// Ler da nova coluna; fallback para a antiga
return $this->full_name ?? ($this->first_name . ' ' . $this->last_name);
}
}

Passo 4: Remover colunas antigas (após todos os pods estarem no novo código)

-- Migração 003: remove_old_name_columns (executar após o deploy completo)
ALTER TABLE users DROP COLUMN first_name;
ALTER TABLE users DROP COLUMN last_name;

3. Adicionar Restrição NOT NULL (Multi-Etapas)

-- Passo 1: adicionar a coluna como nullable
ALTER TABLE orders ADD COLUMN customer_id INTEGER;
-- Passo 2: fazer backfill das linhas existentes
UPDATE orders SET customer_id = (
SELECT id FROM customers WHERE customers.email = orders.customer_email
);
-- Passo 3: adicionar a restrição (após o backfill estar concluído)
ALTER TABLE orders ALTER COLUMN customer_id SET NOT NULL;
-- Passo 4: adicionar foreign key
ALTER TABLE orders ADD CONSTRAINT fk_orders_customer
FOREIGN KEY (customer_id) REFERENCES customers(id);

4. Remover uma Coluna (Sequência Segura)

// Passo 1: deixar de ler a coluna no código
// Remover todas as queries SELECT que incluam a coluna
// Passo 2: deixar de escrever na coluna
// Remover todas as instruções INSERT/UPDATE que definam a coluna
// Passo 3: fazer deploy e verificar que não há erros
// Passo 4: remover a coluna (migração)
-- Migração: drop_legacy_status_column
-- Executar apenas após as alterações de código estarem em produção
ALTER TABLE orders DROP COLUMN legacy_status;

Ferramentas de Migração Específicas por Framework

Laravel Migrations

// database/migrations/2024_01_15_create_orders_table.php
class CreateOrdersTable extends Migration
{
public function up()
{
Schema::create('orders', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained()->onDelete('cascade');
$table->string('status')->default('pending');
$table->decimal('total', 10, 2);
$table->timestamps();
$table->softDeletes();
$table->index(['user_id', 'status']);
});
}
public function down()
{
Schema::dropIfExists('orders');
}
}
// Executar migrações
php artisan migrate
// Rollback do último batch
php artisan migrate:rollback
// Rollback de passos específicos
php artisan migrate:rollback --step=3

Ruby on Rails Migrations

# db/migrate/20240115_add_tracking_to_orders.rb
class AddTrackingToOrders < ActiveRecord::Migration[7.0]
# Desativar transação para tabelas grandes
disable_ddl_transaction!
def change
# Adicionar coluna com índice concorrente
add_column :orders, :tracking_number, :string
add_index :orders, :tracking_number,
algorithm: :concurrently,
if_not_exists: true
end
end
# Executar migrações
rails db:migrate
# Rollback
rails db:rollback STEP=1

Django Migrations

# orders/migrations/0002_add_tracking.py
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('orders', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='order',
name='tracking_number',
field=models.CharField(max_length=100, null=True, blank=True),
),
migrations.AddIndex(
model_name='order',
index=models.Index(fields=['tracking_number'], name='tracking_idx'),
),
]
# Executar migrações
python manage.py migrate
# Criar migração a partir de alterações no model
python manage.py makemigrations
# Rollback
python manage.py migrate orders 0001

Migrações em Tabelas Grandes

Alteração de Esquema Online com pt-online-schema-change (MySQL)

# Adicionar coluna a uma tabela grande sem bloquear
pt-online-schema-change \
--alter "ADD COLUMN new_field VARCHAR(255)" \
--execute \
--host=localhost \
--user=root \
--ask-pass \
D=mydb,t=large_table

Índice Concorrente no PostgreSQL

-- Criar índice sem bloquear escritas
CREATE INDEX CONCURRENTLY idx_orders_created ON orders(created_at);
-- Se falhar, remover primeiro o índice inválido
DROP INDEX CONCURRENTLY IF EXISTS idx_orders_created;

Atualizações em Lote para Tabelas Grandes

// Atualização em lote no Laravel
class BackfillUserSettings extends Migration
{
public function up()
{
// Processar em lotes para evitar problemas de memória
User::query()
->whereNull('settings')
->chunkById(1000, function ($users) {
foreach ($users as $user) {
$user->update(['settings' => json_encode(['theme' => 'light'])]);
}
});
}
}
# Atualização em lote no Rails
class BackfillUserSettings < ActiveRecord::Migration[7.0]
disable_ddl_transaction!
def up
User.where(settings: nil).find_in_batches(batch_size: 1000) do |batch|
User.where(id: batch.map(&:id)).update_all(settings: '{"theme": "light"}')
sleep(0.1) # Reduzir a carga na base de dados
end
end
end

Migração no Pipeline de CI/CD

Job de Migração no GitLab CI

migrate-database:
stage: deploy
image: myapp:${CI_COMMIT_SHA}
script:
# Verificar migrações pendentes
- php artisan migrate:status
# Executar migrações
- php artisan migrate --force
# Verificar migração
- php artisan migrate:status | grep -q "No pending migrations"
rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
changes:
- database/migrations/**/*
environment:
name: production

Kubernetes Job para Migrações

apiVersion: batch/v1
kind: Job
metadata:
name: db-migration-${CI_COMMIT_SHA}
namespace: production
spec:
backoffLimit: 0
template:
spec:
restartPolicy: Never
initContainers:
- name: wait-for-db
image: busybox
command: ['sh', '-c', 'until nc -z postgres 5432; do sleep 2; done']
containers:
- name: migrate
image: myapp:${CI_COMMIT_SHA}
command: ["php", "artisan", "migrate", "--force"]
envFrom:
- secretRef:
name: app-secrets

Estratégias de Rollback de Migração

Suporte de Rollback ao Nível da Aplicação

// Feature flag para rollback
class OrderController extends Controller
{
public function store(Request $request)
{
$order = new Order();
$order->user_id = auth()->id();
$order->total = $request->total;
// Novo campo, com feature flag
if (Feature::active('new-order-tracking')) {
$order->tracking_number = $this->generateTracking();
}
$order->save();
return response()->json($order);
}
}

Snapshot da Base de Dados Antes da Migração

#!/bin/bash
# pre-migration-backup.sh
set -e
BACKUP_NAME="pre-migration-$(date +%Y%m%d-%H%M%S)"
# Criar snapshot
pg_dump $DATABASE_URL > /tmp/${BACKUP_NAME}.sql
# Carregar para o S3
aws s3 cp /tmp/${BACKUP_NAME}.sql s3://db-backups/migrations/${BACKUP_NAME}.sql
# Guardar o nome do backup para eventual rollback
echo $BACKUP_NAME > /tmp/last-migration-backup
echo "Backup created: $BACKUP_NAME"

Testes de Migração

Testar Migrações em CI

test-migrations:
stage: test
services:
- postgres:15
variables:
DATABASE_URL: postgres://test:test@postgres:5432/test
script:
# Aplicar todas as migrações
- php artisan migrate --force
# Executar migrações down
- php artisan migrate:rollback --step=999
# Executar up novamente
- php artisan migrate --force
# Semear (seed) e verificar
- php artisan db:seed
- php artisan test --filter=DatabaseTest

Testes com Base de Dados Shadow

test-migration-on-copy:
stage: pre-deploy
script:
# Criar cópia da base de dados de produção
- pg_dump $PROD_DATABASE_URL | psql $SHADOW_DATABASE_URL
# Executar migração na shadow
- DATABASE_URL=$SHADOW_DATABASE_URL php artisan migrate --force
# Executar smoke tests
- DATABASE_URL=$SHADOW_DATABASE_URL php artisan test --filter=SmokeTest
# Remover a base de dados shadow
- psql $SHADOW_DATABASE_URL -c "DROP DATABASE shadow_db"

Checklist de Migração

Antes da Migração

  • [ ] Confirmar a propriedade dos dados e as dependências
  • [ ] Migração testada em ambiente de staging com dados semelhantes aos de produção
  • [ ] Script de rollback testado
  • [ ] Backup da base de dados criado e verificado com um exercício de restauro
  • [ ] Equipa notificada da janela de migração
  • [ ] Dashboards de monitorização prontos

Durante a Migração

  • [ ] Monitorizar CPU e I/O da base de dados
  • [ ] Monitorizar taxas de erro da aplicação
  • [ ] Monitorizar latência das queries
  • [ ] Monitorizar tempos de espera por locks
  • [ ] Verificar replication lag (se aplicável)

Após a Migração

  • [ ] Verificar que a migração foi concluída com sucesso
  • [ ] Executar smoke tests
  • [ ] Verificar logs da aplicação quanto a erros
  • [ ] Executar verificações de reconciliação (comparar contagens de registos, somas ou hashes)
  • [ ] Atualizar a documentação
  • [ ] Limpar colunas/tabelas antigas (após período de tolerância)

A Mentalidade de um Senior de Base de Dados

Engenheiros seniores tratam as bases de dados como infraestrutura crítica. Investem em:

  • Backups e exercícios de recuperação - Nunca executar uma migração grande sem um backup verificado
  • Clareza na propriedade dos dados - Antes de alterar uma base de dados, definir que sistema é dono de cada dataset
  • Padrões seguros de evolução do esquema - Passos aditivos, backward-compatible, com pontos de rollback
  • Verificações de reconciliação - Comparação automatizada de contagens de registos e integridade dos dados

Armadilhas Comuns a Evitar

  • Executar migrações grandes sem backups testados
  • Introduzir alterações de esquema incompatíveis sem versionamento
  • Ignorar replication lag durante backfills pesados
  • Adicionar índices sem validar o impacto nas queries
  • Falhar na documentação dos passos de migração

Trate as migrações como alterações de produção, não como scripts pontuais. Quando as operações de base de dados são previsíveis e seguras, todo o sistema se torna mais fácil de evoluir.

Artigos Relacionados na Wiki

 
 
 
Языки
Темы
Copyright © 1999 — 2026
ZK Interactive