SQLAlchemy 2.0: ORM Moderno para Python com Tipagem
Aprenda SQLAlchemy 2.0 com o novo estilo declarativo tipado com Mapped[], suporte async nativo e integração com FastAPI. Guia prático com exemplos.
O SQLAlchemy é o ORM mais utilizado no ecossistema Python, presente em projetos que vão de APIs simples a sistemas de grande escala. Com o lançamento da versão 2.0, a biblioteca passou por uma reformulação significativa: novo estilo declarativo com tipagem nativa, API de queries unificada, suporte a async/await e integração profunda com ferramentas de análise estática.
Se você já trabalha com PostgreSQL, SQLite ou qualquer banco relacional em Python, entender o SQLAlchemy 2.0 é fundamental. Neste artigo, vamos cobrir desde a configuração inicial até padrões avançados como relacionamentos, queries compostas e integração assíncrona.
Por que SQLAlchemy 2.0?
A versão 2.0 não é uma simples atualização incremental. Ela redefine como você escreve modelos e queries, alinhando o SQLAlchemy com as práticas modernas do Python:
- Tipagem nativa com
Mapped[]emapped_column()— IDEs como VSCode e PyCharm entendem seus modelos sem plugins - API unificada entre Core e ORM — uma única forma de construir queries
- Async nativo — suporte completo a
async/awaitsem hacks - Melhor performance — uso otimizado de RETURNING e batch inserts
# Estilo antigo (1.x) - ainda funciona, mas não recomendado
from sqlalchemy import Column, Integer, String
from sqlalchemy.ext.declarative import declarative_base
Base = declarative_base()
class Usuario(Base):
__tablename__ = "usuarios"
id = Column(Integer, primary_key=True)
nome = Column(String(100))
email = Column(String(200))
# Estilo novo (2.0) - tipado, moderno, recomendado
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
class Base(DeclarativeBase):
pass
class Usuario(Base):
__tablename__ = "usuarios"
id: Mapped[int] = mapped_column(primary_key=True)
nome: Mapped[str] = mapped_column(String(100))
email: Mapped[str] = mapped_column(String(200), unique=True)
A diferença principal é que no estilo 2.0, seus modelos são classes Python com anotações de tipo reais. Ferramentas como mypy e ty conseguem validar seu código sem configuração adicional.
Configuração inicial
Comece instalando o SQLAlchemy e um driver de banco de dados:
# SQLAlchemy com driver SQLite (incluso no Python)
pip install sqlalchemy
# Para PostgreSQL
pip install sqlalchemy psycopg2-binary
# Para MySQL
pip install sqlalchemy pymysql
# Ou usando uv (mais rápido)
uv pip install sqlalchemy psycopg2-binary
A conexão com o banco usa o objeto Engine:
from sqlalchemy import create_engine
# SQLite (arquivo local)
engine = create_engine("sqlite:///app.db", echo=True)
# PostgreSQL
engine = create_engine(
"postgresql://usuario:senha@localhost:5432/meu_banco",
pool_size=10,
max_overflow=20,
)
# Criar todas as tabelas definidas nos modelos
Base.metadata.create_all(engine)
O parâmetro echo=True mostra as queries SQL geradas no terminal, útil durante o desenvolvimento e depuração.
Definindo modelos com Mapped[]
O novo estilo declarativo usa Mapped[] para anotar cada coluna. Isso traz benefícios reais para autocompletar e validação estática:
from datetime import datetime
from typing import Optional
from sqlalchemy import String, Text, ForeignKey
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship
class Base(DeclarativeBase):
pass
class Autor(Base):
__tablename__ = "autores"
id: Mapped[int] = mapped_column(primary_key=True)
nome: Mapped[str] = mapped_column(String(100))
email: Mapped[str] = mapped_column(String(200), unique=True)
bio: Mapped[Optional[str]] = mapped_column(Text, default=None)
ativo: Mapped[bool] = mapped_column(default=True)
criado_em: Mapped[datetime] = mapped_column(default_factory=datetime.utcnow)
# Relacionamento: um autor tem muitos artigos
artigos: Mapped[list["Artigo"]] = relationship(back_populates="autor")
def __repr__(self) -> str:
return f"Autor(id={self.id}, nome={self.nome!r})"
class Artigo(Base):
__tablename__ = "artigos"
id: Mapped[int] = mapped_column(primary_key=True)
titulo: Mapped[str] = mapped_column(String(200))
conteudo: Mapped[str] = mapped_column(Text)
publicado: Mapped[bool] = mapped_column(default=False)
autor_id: Mapped[int] = mapped_column(ForeignKey("autores.id"))
criado_em: Mapped[datetime] = mapped_column(default_factory=datetime.utcnow)
# Relacionamento inverso
autor: Mapped["Autor"] = relationship(back_populates="artigos")
def __repr__(self) -> str:
return f"Artigo(id={self.id}, titulo={self.titulo!r})"
Note como Mapped[Optional[str]] indica um campo nullable, enquanto Mapped[str] indica NOT NULL. O type checker consegue inferir isso automaticamente.
CRUD com a nova API de sessions
O SQLAlchemy 2.0 usa Session com context managers para gerenciar transações de forma segura:
from sqlalchemy.orm import Session
# CREATE - inserir registros
with Session(engine) as session:
autor = Autor(nome="Ana Silva", email="[email protected]")
session.add(autor)
artigos = [
Artigo(titulo="Introdução ao Python", conteudo="...", autor=autor),
Artigo(titulo="FastAPI na Prática", conteudo="...", autor=autor),
]
session.add_all(artigos)
session.commit()
# READ - consultar registros
with Session(engine) as session:
# Buscar por chave primária
autor = session.get(Autor, 1)
print(autor.nome)
# Query com filtros
from sqlalchemy import select
stmt = select(Autor).where(Autor.ativo == True).order_by(Autor.nome)
autores = session.scalars(stmt).all()
for a in autores:
print(f"{a.nome} - {len(a.artigos)} artigos")
# UPDATE - atualizar registros
with Session(engine) as session:
autor = session.get(Autor, 1)
autor.bio = "Desenvolvedora Python com 5 anos de experiência"
session.commit()
# DELETE - remover registros
with Session(engine) as session:
artigo = session.get(Artigo, 2)
session.delete(artigo)
session.commit()
O uso de context managers (with) garante que a sessão seja fechada corretamente, mesmo em caso de exceções.
Queries avançadas com select()
A nova API select() é poderosa e composável. Você pode construir queries complexas de forma legível:
from sqlalchemy import select, func, and_, or_, desc
with Session(engine) as session:
# Contagem de artigos por autor
stmt = (
select(Autor.nome, func.count(Artigo.id).label("total_artigos"))
.join(Artigo)
.group_by(Autor.nome)
.having(func.count(Artigo.id) >= 2)
.order_by(desc("total_artigos"))
)
resultados = session.execute(stmt).all()
for nome, total in resultados:
print(f"{nome}: {total} artigos")
# Busca com filtros combinados
stmt = (
select(Artigo)
.join(Autor)
.where(
and_(
Artigo.publicado == True,
or_(
Artigo.titulo.contains("Python"),
Artigo.titulo.contains("FastAPI"),
),
Autor.ativo == True,
)
)
.order_by(Artigo.criado_em.desc())
.limit(10)
)
artigos = session.scalars(stmt).all()
# Subquery: autores com mais artigos que a média
media_artigos = (
select(func.avg(func.count(Artigo.id)))
.group_by(Artigo.autor_id)
.scalar_subquery()
)
stmt = (
select(Autor)
.join(Artigo)
.group_by(Autor.id)
.having(func.count(Artigo.id) > media_artigos)
)
top_autores = session.scalars(stmt).all()
Essa API é a mesma usada no Core e no ORM, eliminando a confusão que existia na versão 1.x entre session.query() e select().
Suporte async nativo
O SQLAlchemy 2.0 tem suporte nativo a async/await, essencial para frameworks como FastAPI:
from sqlalchemy.ext.asyncio import (
create_async_engine,
async_sessionmaker,
AsyncSession,
)
# Engine assíncrono (note o driver asyncpg para PostgreSQL)
async_engine = create_async_engine(
"postgresql+asyncpg://usuario:senha@localhost:5432/meu_banco",
pool_size=10,
)
# Fábrica de sessões assíncronas
AsyncSessionLocal = async_sessionmaker(async_engine, class_=AsyncSession)
async def listar_autores_ativos() -> list[Autor]:
async with AsyncSessionLocal() as session:
stmt = (
select(Autor)
.where(Autor.ativo == True)
.order_by(Autor.nome)
)
resultado = await session.scalars(stmt)
return resultado.all()
async def criar_autor(nome: str, email: str) -> Autor:
async with AsyncSessionLocal() as session:
autor = Autor(nome=nome, email=email)
session.add(autor)
await session.commit()
await session.refresh(autor)
return autor
Para usar com FastAPI, o padrão recomendado é injetar a sessão via dependência:
from fastapi import FastAPI, Depends
app = FastAPI()
async def get_session():
async with AsyncSessionLocal() as session:
yield session
@app.get("/autores")
async def listar_autores(session: AsyncSession = Depends(get_session)):
stmt = select(Autor).where(Autor.ativo == True)
autores = (await session.scalars(stmt)).all()
return [{"id": a.id, "nome": a.nome} for a in autores]
Migrations com Alembic
Em projetos reais, você precisa de um sistema de migrations para evoluir o schema do banco sem perder dados. O Alembic é a ferramenta oficial do SQLAlchemy para isso:
# Instalar
pip install alembic
# Inicializar no projeto
alembic init migrations
Configure o alembic.ini com sua connection string e o migrations/env.py para importar seus modelos:
# migrations/env.py (trecho relevante)
from app.models import Base
target_metadata = Base.metadata
Depois, gere e aplique migrations:
# Gerar migration automática baseada nos modelos
alembic revision --autogenerate -m "criar tabelas iniciais"
# Aplicar migrations pendentes
alembic upgrade head
# Ver histórico
alembic history
# Reverter última migration
alembic downgrade -1
O Alembic detecta automaticamente mudanças nos seus modelos (novas colunas, tabelas, índices) e gera o script de migration correspondente. Isso é equivalente ao que o Django faz com makemigrations, mas funciona com qualquer framework.
Padrões úteis para projetos reais
Mixin para campos comuns
Evite repetição usando mixins para campos presentes em todas as tabelas:
from datetime import datetime
from sqlalchemy.orm import Mapped, mapped_column
class TimestampMixin:
criado_em: Mapped[datetime] = mapped_column(default_factory=datetime.utcnow)
atualizado_em: Mapped[datetime] = mapped_column(
default_factory=datetime.utcnow,
onupdate=datetime.utcnow,
)
class Autor(TimestampMixin, Base):
__tablename__ = "autores"
id: Mapped[int] = mapped_column(primary_key=True)
nome: Mapped[str] = mapped_column(String(100))
# criado_em e atualizado_em vêm do mixin
Repository pattern
Encapsule a lógica de acesso a dados em classes específicas:
class AutorRepository:
def __init__(self, session: Session):
self.session = session
def buscar_por_id(self, autor_id: int) -> Autor | None:
return self.session.get(Autor, autor_id)
def buscar_por_email(self, email: str) -> Autor | None:
stmt = select(Autor).where(Autor.email == email)
return self.session.scalar(stmt)
def listar_ativos(self, limite: int = 50) -> list[Autor]:
stmt = (
select(Autor)
.where(Autor.ativo == True)
.order_by(Autor.nome)
.limit(limite)
)
return list(self.session.scalars(stmt))
def criar(self, nome: str, email: str) -> Autor:
autor = Autor(nome=nome, email=email)
self.session.add(autor)
self.session.flush() # Gera o ID sem commitar
return autor
Esse padrão, combinado com dataclasses ou Pydantic para validação, cria uma arquitetura limpa e testável.
Comparativo: SQLAlchemy vs alternativas
| Recurso | SQLAlchemy 2.0 | Django ORM | Tortoise ORM | SQLModel |
|---|---|---|---|---|
| Tipagem nativa | Sim (Mapped[]) | Parcial | Parcial | Sim |
| Async nativo | Sim | Sim (Django 4.1+) | Sim | Sim |
| Migrations | Alembic | Integrado | Aerich | Alembic |
| Independente de framework | Sim | Não (Django) | Sim | Sim |
| Maturidade | 18+ anos | 18+ anos | 5+ anos | 3+ anos |
| Raw SQL fácil | Sim | Limitado | Limitado | Sim |
O SQLAlchemy continua sendo a escolha mais flexível para projetos que não usam Django. Se você trabalha com FastAPI ou Flask, o SQLAlchemy 2.0 é a opção padrão.
Veja também nosso artigo sobre Marimo para explorar dados de forma interativa com notebooks reativos em Python.
Conclusão
O SQLAlchemy 2.0 modernizou uma das bibliotecas mais importantes do ecossistema Python. A tipagem nativa com Mapped[] melhora a experiência de desenvolvimento de forma tangível: autocompletar funciona, type checkers encontram erros antes da execução e o código fica mais legível.
Se você está começando um projeto novo, use o estilo 2.0 desde o início. Se tem um projeto existente na versão 1.x, a migração pode ser gradual — os dois estilos coexistem. O guia oficial de migração cobre todos os cenários.
A combinação de SQLAlchemy 2.0 com Alembic para migrations, Pydantic para validação e FastAPI para a API cria um stack robusto e moderno para qualquer aplicação Python que precise de banco de dados relacional.
Se você trabalha com múltiplas linguagens, vale conhecer também o GORM em Go e o Exposed em Kotlin — ORMs modernos com type safety que seguem filosofias semelhantes ao SQLAlchemy 2.0.
Equipe python.dev.br
Contribuidor do Python Brasil — Aprenda Python em Português