Wilberhg's blog

Guia Completo de Alembic: Gerenciamento de Migrações

O Alembic é a ferramenta oficial de controle e migração de esquemas de banco de dados para o ecossistema do SQLAlchemy. Ele funciona como um "sistema de versionamento de código" (semelhante ao Git), mas voltado para a estrutura das tabelas, índices, colunas e restrições do seu banco de dados.


1. Por que usar o Alembic?

Em aplicações reais, os modelos de dados evoluem constantemente: novas colunas surgem, tipos mudam e tabelas são criadas ou removidas.


2. Instalação e Inicialização

Instalação

Instale o Alembic e o driver de conexão com o banco de dados desejado (como PostgreSQL, SQLite, MySQL):

# Com pip
pip install alembic sqlalchemy psycopg2-binary

# Ou com uv / poetry
uv add alembic sqlalchemy psycopg2-binary

Inicializando o Diretório do Alembic

No diretório raiz do seu projeto, execute:

alembic init alembic

Isso gerará a estrutura básica de migração:

meu_projeto/
├── alembic.ini             # Configurações globais e string de conexão padrão
├── alembic/                # Diretório do ambiente Alembic
│   ├── env.py              # Script executado a cada comando do Alembic
│   ├── README
│   ├── script.py.mako      # Template Jinja/Mako para gerar as migrações
│   └── versions/           # Pasta onde ficam os scripts de migração gerados
├── src/
│   └── ...
└── pyproject.toml

3. Configuração Inicial Essencial

Para o Alembic funcionar perfeitamente com os seus modelos SQLAlchemy e detectar alterações automaticamente, precisamos configurar dois arquivos:

3.1 alembic.ini (ou Variável de Ambiente)

No alembic.ini, defina a URL do banco de dados na chave sqlalchemy.url:

# alembic.ini
sqlalchemy.url = postgresql+psycopg2://usuario:senha@localhost:5432/meubanco

Dica de Produção: Evite senhas em texto puro no .ini. O padrão recomendado é ler a URL a partir de variáveis de ambiente diretamente dentro do env.py.


3.2 alembic/env.py (Conectando aos Modelos)

O arquivo env.py é o cérebro das migrações. Por padrão, a variável target_metadata vem como None. Para habilitar o modo autogenerate, aponte-a para o Base.metadata do seu projeto.

Exemplo de configuração dinâmica com variáveis de ambiente:

import os
import sys
from logging.config import fileConfig

from sqlalchemy import engine_from_config, pool
from alembic import context

# 1. Garanta que o diretório raiz do projeto esteja no PYTHONPATH
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))

# 2. Importe o Base com os metadados dos seus modelos
from src.infrastructure.database import Base  # noqa
import src.infrastructure.models  # Garante que todos os modelos foram carregados

# Configurações de logging do Alembic
config = context.config

# 3. Sobrescreva a URL do banco com variável de ambiente (se existir)
db_url = os.getenv("DATABASE_URL")
if db_url:
    config.set_main_option("sqlalchemy.url", db_url)

if config.config_file_name is not None:
    fileConfig(config.config_file_name)

# 4. Vincule os metadados para que o autogenerate funcione!
target_metadata = Base.metadata


def run_migrations_offline() -> None:
    """Modo offline: gera o SQL puro sem conectar diretamente ao banco."""
    url = config.get_main_option("sqlalchemy.url")
    context.configure(
        url=url,
        target_metadata=target_metadata,
        literal_binds=True,
        dialect_opts={"paramstyle": "named"},
    )

    with context.begin_transaction():
        context.run_migrations()


def run_migrations_online() -> None:
    """Modo online: conecta ao banco e aplica as migrações."""
    connectable = engine_from_config(
        config.get_section(config.config_ini_section, {}),
        prefix="sqlalchemy.",
        poolclass=pool.NullPool,
    )

    with connectable.connect() as connection:
        context.configure(
            connection=connection,
            target_metadata=target_metadata,
            compare_type=True,  # Opcional: detecta alterações de tipo de coluna
        )

        with context.begin_transaction():
            context.run_migrations()


if context.is_offline_mode():
    run_migrations_offline()
else:
    run_migrations_online()

4. O Fluxo de Trabalho de Migrações (Workflow Diário)

O ciclo de vida de desenvolvimento com Alembic resume-se a três passos simples:

Alterar Modelos Python ➔ Gerar Revisão (alembic revision) ➔ Aplicar no Banco (alembic upgrade)

Passo 1: Criar ou Modificar um Modelo

Exemplo em src/infrastructure/models.py:

from sqlalchemy import Column, Integer, String, DateTime, func
from src.infrastructure.database import Base

class Usuario(Base):
    __tablename__ = "usuarios"

    id = Column(Integer, primary_key=True)
    nome = Column(String(100), nullable=False)
    email = Column(String(255), unique=True, nullable=False, index=True)
    criado_em = Column(DateTime, server_default=func.now(), nullable=False)

Passo 2: Gerar o Arquivo de Migração Automaticamente

Execute o comando revision com a flag --autogenerate:

alembic revision --autogenerate -m "criar tabela de usuarios"

O Alembic comparará o estado do banco atual com os modelos SQLAlchemy e criará um arquivo em alembic/versions/ (ex: 9a8b7c6d5e4f_criar_tabela_de_usuarios.py):

"""criar tabela de usuarios

Revision ID: 9a8b7c6d5e4f
Revises: 
Create Date: 2025-01-15 10:30:00.000000

"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa

revision: str = '9a8b7c6d5e4f'
down_revision: Union[str, None] = None
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None


def upgrade() -> None:
    op.create_table(
        'usuarios',
        sa.Column('id', sa.Integer(), nullable=False),
        sa.Column('nome', sa.String(length=100), nullable=False),
        sa.Column('email', sa.String(length=255), nullable=False),
        sa.Column('criado_em', sa.DateTime(), server_default=sa.text('now()'), nullable=False),
        sa.PrimaryKeyConstraint('id')
    )
    op.create_index(op.f('ix_usuarios_email'), 'usuarios', ['email'], unique=True)


def downgrade() -> None:
    op.drop_index(op.f('ix_usuarios_email'), table_name='usuarios')
    op.drop_table('usuarios')

Sempre revise o arquivo gerado! O recurso de autogenerate é excelente, mas algumas operações complexas (como renomeação de colunas) podem ser interpretadas como uma remoção (drop) seguida de adição (add).

Passo 3: Aplicar a Migração ao Banco

Para executar todas as migrações pendentes até a versão mais recente:

alembic upgrade head

Para aplicar apenas a próxima migração:

alembic upgrade +1

5. Revertendo e Inspecionando Migrações

Revertendo Migrações (downgrade)

Para desfazer a última migração aplicada:

alembic downgrade -1

Para voltar o banco ao estado inicial (remover todas as migrações):

alembic downgrade base

Para voltar a uma revisão específica:

alembic downgrade <revision_id>

Comandos de Inspeção e Diagnóstico


6. Onde o Alembic fica na Estrutura do Projeto?

A convenção recomendada é manter o diretório alembic/ e o arquivo alembic.ini na raiz do repositório, desacoplados do código de aplicação:

meu_projeto/
├── alembic.ini                   # Configuração do Alembic na raiz
├── alembic/                      # Migrações separadas do código fonte
│   ├── env.py                    # Script de execução
│   ├── script.py.mako            # Template das revisões
│   └── versions/                 # Scripts de migração (versionados no Git)
│       ├── 20250115_01_init.py
│       └── 20250120_02_add_status.py
│
├── src/                          # Código da aplicação
│   ├── core/
│   │   └── config.py             # Lê configs (incluindo DATABASE_URL)
│   ├── domain/
│   │   └── enums/
│   └── infrastructure/
│       ├── database.py           # engine, sessionmaker, Base
│       └── models/               # Modelos SQLAlchemy mapeados
│           ├── __init__.py
│           ├── usuario.py
│           └── pedido.py
│
├── tests/
├── pyproject.toml
└── README.md

7. O que o autogenerate Detecta (e o que NÃO detecta)

O que o Autogenerate detecta automaticamente:

O que NÃO detecta automaticamente (ou requer atenção):


8. Boas Práticas em Ambientes de Produção

  1. Nunca edite uma migração já enviada para produção ou para a branch principal: Sempre crie uma nova revisão com alembic revision para corrigir ou alterar o esquema.
  2. Versionar a pasta versions/ no Git: Toda a equipe e o pipeline de CI/CD devem compartilhar o mesmo histórico de migrações.
  3. Execute migrações em CI/CD antes do deploy da aplicação: No pipeline de deploy, rode alembic upgrade head antes de subir os contêineres que consom a nova estrutura de tabelas.
  4. Utilize Naming Conventions no SQLAlchemy: Adicione convenções de nomenclatura explícitas aos metadados do SQLAlchemy para evitar problemas com geração de nomes de índices e chaves estrangeiras:
    from sqlalchemy import MetaData
    
    convention = {
        "ix": "ix_%(column_0_label)s",
        "uq": "uq_%(table_name)s_%(column_0_name)s",
        "ck": "ck_%(table_name)s_%(constraint_name)s",
        "fk": "fk_%(table_name)s_%(column_0_name)s_%(referred_table_name)s",
        "pk": "pk_%(table_name)s"
    }
    
    metadata = MetaData(naming_convention=convention)
    Base = declarative_base(metadata=metadata)