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?
- Você modifica seu modelo SQLModel (adiciona campo, muda tipo, etc.)
- Alembic detecta a mudança automaticamente comparando o modelo com o banco
- Gera um script de migração com as operações SQL necessárias
- 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
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:
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
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:
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}")
|
✅ 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
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.