Alembic e SQLModel: Migrações de Banco de Dados em Python

Alembic e SQLModel: Migrações de Banco de Dados em Python

11 min de leitura

Se você já trabalhou com bancos de dados em Python, provavelmente conhece a dor de gerenciar mudanças de schema. Adicionar uma coluna aqui, modificar um tipo ali, criar um índice acolá… e de repente você não sabe mais qual é o estado real do seu banco. Pior ainda: como garantir que todos os ambientes (desenvolvimento, staging, produção) estão sincronizados?

A combinação de SQLModel e Alembic resolve esse problema de forma elegante, trazendo type safety com Pydantic, a robustez do SQLAlchemy, e controle de versão para suas migrações de banco de dados. Neste artigo, você vai aprender como configurar e usar essas duas ferramentas poderosas em conjunto.

O Problema: Schema Manual e Caos

No modelo tradicional, mudanças no banco de dados são feitas manualmente via scripts SQL:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
# Abordagem problemática - NUNCA faça isso!
import sqlite3

def atualizar_schema():
    conn = sqlite3.connect('database.db')
    cursor = conn.cursor()
    
    # E se essa coluna já existir?
    cursor.execute("ALTER TABLE users ADD COLUMN email VARCHAR(255)")
    
    # E se falhar no meio? Como reverter?
    cursor.execute("ALTER TABLE users ADD COLUMN created_at TIMESTAMP")
    
    conn.commit()
    conn.close()

Problemas dessa abordagem:

Sem controle de versão - Você não sabe quais mudanças já foram aplicadas
Sem rollback - Se algo der errado, boa sorte desfazendo manualmente
Sem validação - Erros só aparecem em runtime
Ambientes dessincronizados - Dev, staging e produção com schemas diferentes
Trabalho em equipe complicado - Como sincronizar mudanças de múltiplos desenvolvedores?

SQLModel: O Melhor dos Dois Mundos

SQLModel é uma biblioteca criada por Sebastian Ramirez (o mesmo criador do FastAPI) que combina o melhor do Pydantic (validação de tipos) com o melhor do SQLAlchemy (ORM poderoso).

Por Que SQLModel?

Type Safety - Usa type hints do Python para validação automática
DRY (Don’t Repeat Yourself) - Um modelo serve tanto para validação quanto para ORM
Editor Support - Autocompletar e verificação de tipos na sua IDE
Compatible - Totalmente compatível com SQLAlchemy e Pydantic

Definindo um Modelo SQLModel

Veja como é simples definir um modelo:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
from typing import Optional
from sqlmodel import SQLModel, Field

class Usuario(SQLModel, table=True):
    """Modelo de usuário no banco de dados"""
    id: Optional[int] = Field(default=None, primary_key=True)
    nome: str = Field(index=True)
    email: str = Field(unique=True, index=True)
    idade: Optional[int] = Field(default=None)
    ativo: bool = Field(default=True)

Esse modelo é simultaneamente:

  • Um modelo Pydantic para validação de dados
  • Um modelo SQLAlchemy para operações no banco
  • Uma definição de schema que pode ser versionada

Alembic: Controle de Versão para Seu Banco de Dados

Alembic é uma ferramenta de migração de banco de dados para SQLAlchemy. Pense nele como um Git para seu schema - ele versiona todas as mudanças e permite aplicá-las ou revertê-las de forma controlada.

Como Funciona?

  1. Você modifica seu modelo SQLModel (adiciona campo, muda tipo, etc.)
  2. Alembic detecta a mudança automaticamente comparando o modelo com o banco
  3. Gera um script de migração com as operações SQL necessárias
  4. Você revisa e aplica a migração de forma controlada

Cada migração tem:

  • Um ID único (hash)
  • Uma função upgrade() para aplicar a mudança
  • Uma função downgrade() para reverter a mudança
  • Referência para a migração anterior (criando uma cadeia de versões)

Mãos à Obra: Configuração Inicial

Vamos criar um projeto do zero e configurar tudo corretamente.

1. Instalação

1
pip install sqlmodel alembic

Para PostgreSQL (produção):

1
pip install psycopg2-binary

2. Estrutura do Projeto

projeto/
├── app/
│   ├── __init__.py
│   ├── models.py      # Modelos SQLModel
│   └── database.py    # Configuração do banco
├── alembic/
│   └── versions/      # Migrações vão aqui (criado automaticamente)
├── alembic.ini        # Configuração do Alembic
└── main.py            # Aplicação principal

3. Inicializando o Alembic

1
alembic init alembic

Isso cria a estrutura de diretórios e arquivos de configuração.

4. Configurando o Alembic para SQLModel

Passo 1: Configure a URL do banco em alembic.ini:

1
2
3
4
5
6
7
8
9
# alembic.ini
[alembic]
# ... outras configurações ...

# Para SQLite (desenvolvimento)
sqlalchemy.url = sqlite:///./database.db

# Para PostgreSQL (produção)
# sqlalchemy.url = postgresql://user:password@localhost/dbname

Passo 2: Configure o env.py para usar SQLModel metadata:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
# alembic/env.py
from logging.config import fileConfig
from sqlalchemy import engine_from_config, pool
from alembic import context

# Importar SQLModel e seus modelos
from sqlmodel import SQLModel
from app.models import Usuario  # Importar TODOS os modelos aqui

config = context.config

# Adiciona o arquivo de configuração para registro de logs em Python
if config.config_file_name is not None:
    fileConfig(config.config_file_name)

# Configurar metadata do SQLModel para autogenerate
target_metadata = SQLModel.metadata

# ... resto do código gerado pelo Alembic ...

IMPORTANTE: Você precisa importar todos os seus modelos no env.py, caso contrário o Alembic não vai detectá-los para autogenerate.

5. Criar Modelos Iniciais

Crie app/models.py:

1
2
3
4
5
6
7
8
9
from typing import Optional
from sqlmodel import SQLModel, Field

class Usuario(SQLModel, table=True):
    """Representa um usuário no sistema"""
    id: Optional[int] = Field(default=None, primary_key=True)
    nome: str = Field(index=True)
    email: str = Field(unique=True, index=True)
    ativo: bool = Field(default=True)

Crie app/database.py:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
from sqlmodel import create_engine, SQLModel, Session

# URL de conexão do banco
DATABASE_URL = "sqlite:///./database.db"

# Criar engine
engine = create_engine(DATABASE_URL, echo=True)

def get_session():
    """Retorna uma sessão do banco de dados"""
    with Session(engine) as session:
        yield session

Primeira Migração: Criando o Schema Inicial

Agora vamos criar nossa primeira migração com autogenerate - o Alembic vai detectar automaticamente os modelos:

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

Você verá algo como:

INFO  [alembic.runtime.migration] Context impl SQLiteImpl.
INFO  [alembic.runtime.migration] Will assume non-transactional DDL.
INFO  [alembic.autogenerate.compare] Detected added table 'usuario'
INFO  [alembic.autogenerate.compare] Detected added index 'ix_usuario_email' on '['email']'
INFO  [alembic.autogenerate.compare] Detected added index 'ix_usuario_nome' on '['nome']'
  Generating /projeto/alembic/versions/abc123_criar_tabela_usuarios.py ...  done

Sempre revise o arquivo gerado! Abra alembic/versions/abc123_criar_tabela_usuarios.py:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
"""criar tabela usuarios

Revision ID: abc123
Revises: 
Create Date: 2026-04-09 10:30:00.000000

"""
from alembic import op
import sqlalchemy as sa
import sqlmodel

# revision identifiers, used by Alembic.
revision = 'abc123'
down_revision = None
branch_labels = None
depends_on = None


def upgrade() -> None:
    """Aplicar mudanças"""
    op.create_table(
        'usuario',
        sa.Column('id', sa.Integer(), nullable=False),
        sa.Column('nome', sa.String(), nullable=False),
        sa.Column('email', sa.String(), nullable=False),
        sa.Column('ativo', sa.Boolean(), nullable=False),
        sa.PrimaryKeyConstraint('id')
    )
    op.create_index(op.f('ix_usuario_email'), 'usuario', ['email'], unique=True)
    op.create_index(op.f('ix_usuario_nome'), 'usuario', ['nome'], unique=False)


def downgrade() -> None:
    """Reverter mudanças"""
    op.drop_index(op.f('ix_usuario_nome'), table_name='usuario')
    op.drop_index(op.f('ix_usuario_email'), table_name='usuario')
    op.drop_table('usuario')

Perfeito! Agora aplique a migração:

1
alembic upgrade head

Output:

INFO  [alembic.runtime.migration] Running upgrade  -> abc123, criar tabela usuarios

Pronto! Sua tabela foi criada. Você pode verificar:

1
2
# Para SQLite
sqlite3 database.db ".schema usuario"

Ou via Python

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
from app.database import engine
from sqlmodel import Session, select
from app.models import Usuario

# Criar um usuário de teste
with Session(engine) as session:
    usuario = Usuario(nome="João Silva", email="joao@example.com")
    session.add(usuario)
    session.commit()
    print(f"✅ Usuário criado com ID: {usuario.id}")

Evoluindo o Schema: Adicionando Campos

Agora vamos adicionar novos campos ao nosso modelo. Imagine que precisamos armazenar a data de criação e o CPF do usuário.

1. Modificar o Modelo

Atualize app/models.py:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
from typing import Optional
from datetime import datetime
from sqlmodel import SQLModel, Field

class Usuario(SQLModel, table=True):
    """Representa um usuário no sistema"""
    id: Optional[int] = Field(default=None, primary_key=True)
    nome: str = Field(index=True)
    email: str = Field(unique=True, index=True)
    cpf: Optional[str] = Field(default=None, unique=True, max_length=11)  # NOVO
    ativo: bool = Field(default=True)
    criado_em: datetime = Field(default_factory=datetime.utcnow)  # NOVO

2. Gerar Nova Migração

1
alembic revision --autogenerate -m "adicionar cpf e criado_em"

O Alembic detecta automaticamente as mudanças:

INFO  [alembic.autogenerate.compare] Detected added column 'usuario.cpf'
INFO  [alembic.autogenerate.compare] Detected added column 'usuario.criado_em'
  Generating /projeto/alembic/versions/def456_adicionar_cpf_e_criado_em.py ...  done

3. Revisar a Migração Gerada

Abra o arquivo gerado:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
"""adicionar cpf e criado_em

Revision ID: def456
Revises: abc123
Create Date: 2026-04-09 11:00:00.000000

"""
from alembic import op
import sqlalchemy as sa
import sqlmodel

revision = 'def456'
down_revision = 'abc123'  # Aponta para a migração anterior
branch_labels = None
depends_on = None


def upgrade() -> None:
    op.add_column('usuario', sa.Column('cpf', sa.String(length=11), nullable=True))
    op.add_column('usuario', sa.Column('criado_em', sa.DateTime(), nullable=False))
    op.create_unique_constraint('uq_usuario_cpf', 'usuario', ['cpf'])


def downgrade() -> None:
    op.drop_constraint('uq_usuario_cpf', 'usuario', type_='unique')
    op.drop_column('usuario', 'criado_em')
    op.drop_column('usuario', 'cpf')

ATENÇÃO: Note que criado_em está como nullable=False, mas estamos adicionando a coluna numa tabela que pode ter dados. Isso pode dar erro! Vamos corrigir:

1
2
3
4
5
6
def upgrade() -> None:
    op.add_column('usuario', sa.Column('cpf', sa.String(length=11), nullable=True))
    # Corrigir: permitir NULL temporariamente ou definir um valor padrão
    op.add_column('usuario', sa.Column('criado_em', sa.DateTime(), 
                  nullable=False, server_default=sa.func.now()))
    op.create_unique_constraint('uq_usuario_cpf', 'usuario', ['cpf'])

4. Aplicar a Migração

1
alembic upgrade head

Sucesso! Agora temos as novas colunas

Relacionamentos Entre Tabelas

Vamos adicionar uma tabela de Post relacionada a Usuario (one-to-many).

1. Definir Novos Modelos

Atualize app/models.py:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
from typing import Optional, List
from datetime import datetime
from sqlmodel import SQLModel, Field, Relationship

class Usuario(SQLModel, table=True):
    """Representa um usuário no sistema"""
    id: Optional[int] = Field(default=None, primary_key=True)
    nome: str = Field(index=True)
    email: str = Field(unique=True, index=True)
    cpf: Optional[str] = Field(default=None, unique=True, max_length=11)
    ativo: bool = Field(default=True)
    criado_em: datetime = Field(default_factory=datetime.utcnow)
    
    # Relacionamento: um usuário tem vários posts
    posts: List["Post"] = Relationship(back_populates="autor")


class Post(SQLModel, table=True):
    """Representa um post/artigo"""
    id: Optional[int] = Field(default=None, primary_key=True)
    titulo: str = Field(index=True)
    conteudo: str
    publicado: bool = Field(default=False)
    criado_em: datetime = Field(default_factory=datetime.utcnow)
    
    # Foreign key
    autor_id: Optional[int] = Field(default=None, foreign_key="usuario.id")
    
    # Relacionamento: um post pertence a um autor
    autor: Optional[Usuario] = Relationship(back_populates="posts")

2. Gerar Migração

1
alembic revision --autogenerate -m "criar tabela posts"

3. Revisar e Aplicar

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
# Arquivo gerado: alembic/versions/ghi789_criar_tabela_posts.py

def upgrade() -> None:
    op.create_table(
        'post',
        sa.Column('id', sa.Integer(), nullable=False),
        sa.Column('titulo', sa.String(), nullable=False),
        sa.Column('conteudo', sa.String(), nullable=False),
        sa.Column('publicado', sa.Boolean(), nullable=False),
        sa.Column('criado_em', sa.DateTime(), nullable=False),
        sa.Column('autor_id', sa.Integer(), nullable=True),
        sa.ForeignKeyConstraint(['autor_id'], ['usuario.id'], ),
        sa.PrimaryKeyConstraint('id')
    )
    op.create_index(op.f('ix_post_titulo'), 'post', ['titulo'], unique=False)

Aplicar:

1
alembic upgrade head

4. Usar os Relacionamentos

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
from app.database import engine
from sqlmodel import Session, select
from app.models import Usuario, Post

with Session(engine) as session:
    # Buscar usuário
    usuario = session.exec(
        select(Usuario).where(Usuario.email == "maria@example.com")
    ).first()
    
    # Criar posts para esse usuário
    post1 = Post(
        titulo="Meu Primeiro Post",
        conteudo="Conteúdo interessante...",
        publicado=True,
        autor_id=usuario.id
    )
    
    post2 = Post(
        titulo="Aprendendo SQLModel",
        conteudo="SQLModel é incrível!",
        publicado=True,
        autor_id=usuario.id
    )
    
    session.add(post1)
    session.add(post2)
    session.commit()
    
    # Acessar posts do usuário via relacionamento
    session.refresh(usuario)
    for post in usuario.posts:
        print(f"📝 Post: {post.titulo}")

Boas Práticas com Alembic e SQLModel

✅ Sempre Revise Migrações Autogenerate

O autogenerate é ótimo, mas não é perfeito. Sempre revise o código gerado antes de aplicar:

1
2
3
4
5
6
7
# Gerar migração
alembic revision --autogenerate -m "mudanca xyz"

# REVISAR o arquivo em alembic/versions/

# Só então aplicar
alembic upgrade head

✅ Teste Downgrades Localmente

Garanta que suas migrações podem ser revertidas:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# Aplicar
alembic upgrade head

# Reverter última migração
alembic downgrade -1

# Reverter tudo
alembic downgrade base

# Reaplicar
alembic upgrade head

✅ Use Nomes Descritivos para Migrações

1
2
3
4
5
# Ruim
alembic revision --autogenerate -m "mudanca"

# Bom
alembic revision --autogenerate -m "adicionar campo email_verificado na tabela usuario"

✅ Mantenha Migrações Pequenas e Atômicas

Ao invés de uma migração gigante mudando 10 tabelas, faça migrações menores e focadas. Isso facilita:

  • Debug quando algo dá errado
  • Rollback parcial se necessário
  • Revisão de código em equipe

✅ Cuidado com Valores Padrão em Colunas NOT NULL

Ao adicionar uma coluna NOT NULL em uma tabela com dados existentes:

1
2
3
4
5
6
# Vai dar erro se a tabela tiver dados
op.add_column('usuario', sa.Column('senha', sa.String(), nullable=False))

# Usar server_default ou fazer em duas etapas
op.add_column('usuario', sa.Column('senha', sa.String(), 
              nullable=False, server_default='temp_password'))

✅ Comandos Úteis do Alembic

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
# Ver histórico de migrações
alembic history

# Ver migração atual
alembic current

# Mostrar SQL que será executado (sem aplicar)
alembic upgrade head --sql

# Ir para uma revisão específica
alembic upgrade abc123

# Informações sobre uma revisão
alembic show abc123

# Criar migração vazia (para mudanças manuais)
alembic revision -m "migração customizada"

Conclusão

A combinação de SQLModel e Alembic oferece uma solução completa e elegante para trabalhar com bancos de dados em Python:

  • SQLModel traz type safety, validação automática e a simplicidade de definir modelos uma única vez
  • Alembic adiciona controle de versão profissional, migrações automáticas e a capacidade de evoluir seu schema com segurança

Principais vantagens dessa stack:

Type Safety - Erros capturados na IDE, não em produção
Controle de Versão - Histórico completo de mudanças no schema
Rollback Confiável - Reverter mudanças quando necessário
Autogenerate Inteligente - Alembic detecta mudanças automaticamente
Trabalho em Equipe - Sincronizar schemas entre desenvolvedores
Ambientes Consistentes - Dev, staging e produção sempre alinhados

Se você está começando um projeto novo ou quer profissionalizar o gerenciamento de banco de dados de um projeto existente, essa combinação é altamente recomendada. O investimento inicial em configuração é pequeno, mas os benefícios a longo prazo são imensos.