<h2>O que é SQLAlchemy ORM e por que você precisa dela</h2>
<p>SQLAlchemy é a biblioteca mais madura e poderosa para trabalhar com bancos de dados em Python. A sigla ORM significa Object-Relational Mapping, ou seja, ela mapeia objetos Python diretamente para tabelas do banco de dados. Em vez de escrever SQL puro, você trabalha com classes Python que representam suas entidades, e a biblioteca cuida da tradução para SQL.</p>
<p>A grande vantagem é que você ganha abstração e segurança. Não precisa se preocupar com sintaxe SQL específica de cada banco de dados, não está vulnerável a SQL injection, e o código fica mais legível e manutenível. Se você precisar mudar de PostgreSQL para MySQL, basta alterar a string de conexão — seu código Python permanece praticamente igual.</p>
<h2>Models: Definindo suas Entidades</h2>
<h3>Estrutura Básica de um Model</h3>
<p>Um Model é uma classe Python que representa uma tabela no banco de dados. Cada atributo da classe é uma coluna, e cada instância é um registro. Você define um Model herdando de uma classe base que SQLAlchemy fornece.</p>
<pre><code class="language-python">from sqlalchemy import create_engine, Column, Integer, String, DateTime
from sqlalchemy.ext.declarative import declarative_base
from datetime import datetime
Criamos a base que todas as nossas classes vão herdar
Base = declarative_base()
class Usuario(Base):
__tablename__ = 'usuarios'
id = Column(Integer, primary_key=True)
nome = Column(String(100), nullable=False)
email = Column(String(120), unique=True, nullable=False)
data_criacao = Column(DateTime, default=datetime.utcnow)
Criamos o engine (conexão com o banco)
engine = create_engine('sqlite:///banco.db')
Criamos as tabelas no banco
Base.metadata.create_all(engine)</code></pre>
<p>O <code>__tablename__</code> define o nome da tabela no banco de dados. O <code>Column</code> especifica tipo, restrições (primary_key, nullable, unique). O <code>engine</code> é responsável por gerenciar a conexão com o banco — neste exemplo, usamos SQLite por simplicidade, mas você pode usar PostgreSQL, MySQL, Oracle, etc.</p>
<h3>Tipos de Dados e Validações</h3>
<p>SQLAlchemy oferece tipos de dados robustos que mapeiam para tipos nativos do banco. Além disso, você pode adicionar validações simples direto nas colunas.</p>
<pre><code class="language-python">from sqlalchemy import Boolean, Float, Text, Enum
import enum
class StatusPedido(enum.Enum):
PENDENTE = "pendente"
PROCESSANDO = "processando"
CONCLUIDO = "concluido"
class Pedido(Base):
__tablename__ = 'pedidos'
id = Column(Integer, primary_key=True)
descricao = Column(Text) # Texto longo
valor = Column(Float, nullable=False) # Números decimais
ativo = Column(Boolean, default=True) # Booleanos
status = Column(Enum(StatusPedido), default=StatusPedido.PENDENTE)
quantidade = Column(Integer, default=1)</code></pre>
<p>Cada tipo de coluna tem seu mapeamento automático para o banco. <code>Float</code> vira DECIMAL, <code>Boolean</code> vira um campo booleano (ou INTEGER em alguns bancos), <code>Enum</code> permite usar enumerações Python de forma segura.</p>
<h2>Relacionamentos: Conectando Models</h2>
<h3>One-to-Many (Um para Muitos)</h3>
<p>Este é o relacionamento mais comum. Um usuário pode ter muitos pedidos, mas cada pedido pertence a apenas um usuário. Você define isso com <code>ForeignKey</code> e <code>relationship</code>.</p>
<pre><code class="language-python">from sqlalchemy import ForeignKey
from sqlalchemy.orm import relationship
class Usuario(Base):
__tablename__ = 'usuarios'
id = Column(Integer, primary_key=True)
nome = Column(String(100), nullable=False)
Aqui vinculamos os pedidos do usuário
pedidos = relationship('Pedido', back_populates='usuario')
class Pedido(Base):
__tablename__ = 'pedidos'
id = Column(Integer, primary_key=True)
descricao = Column(String(255), nullable=False)
usuario_id = Column(Integer, ForeignKey('usuarios.id'), nullable=False)
Aqui referenciamos o usuário
usuario = relationship('Usuario', back_populates='pedidos')</code></pre>
<p>O <code>ForeignKey</code> garante integridade referencial no banco de dados. O <code>relationship</code> é um atributo Python que permite acessar os dados relacionados facilmente. O <code>back_populates</code> cria um link bidirecional — você acessa <code>usuario.pedidos</code> ou <code>pedido.usuario</code> automaticamente.</p>
<h3>Many-to-Many (Muitos para Muitos)</h3>
<p>Quando duas entidades se relacionam de forma bidirecional sem hierarquia, usamos tabelas de associação. Por exemplo, alunos e cursos: um aluno pode estar em vários cursos, e um curso pode ter vários alunos.</p>
<pre><code class="language-python">from sqlalchemy import Table
Tabela de associação (sem classe correspondente)
aluno_curso = Table(
'aluno_curso',
Base.metadata,
Column('aluno_id', Integer, ForeignKey('alunos.id'), primary_key=True),
Column('curso_id', Integer, ForeignKey('cursos.id'), primary_key=True)
)
class Aluno(Base):
__tablename__ = 'alunos'
id = Column(Integer, primary_key=True)
nome = Column(String(100), nullable=False)
cursos = relationship('Curso', secondary=aluno_curso, back_populates='alunos')
class Curso(Base):
__tablename__ = 'cursos'
id = Column(Integer, primary_key=True)
titulo = Column(String(150), nullable=False)
alunos = relationship('Aluno', secondary=aluno_curso, back_populates='cursos')</code></pre>
<p>O argumento <code>secondary</code> aponta para a tabela de junção. Isso permite acesso simples: <code>aluno.cursos</code> retorna uma lista de cursos, sem você precisar escrever queries manualmente.</p>
<h3>One-to-One (Um para Um)</h3>
<p>Menos comum, mas útil quando uma entidade tem exatamente um relacionamento com outra. Usamos <code>uselist=False</code> para isso.</p>
<pre><code class="language-python">class Pessoa(Base):
__tablename__ = 'pessoas'
id = Column(Integer, primary_key=True)
nome = Column(String(100), nullable=False)
passaporte = relationship('Passaporte', uselist=False, back_populates='pessoa')
class Passaporte(Base):
__tablename__ = 'passaportes'
id = Column(Integer, primary_key=True)
numero = Column(String(20), unique=True, nullable=False)
pessoa_id = Column(Integer, ForeignKey('pessoas.id'), unique=True)
pessoa = relationship('Pessoa', back_populates='passaporte')</code></pre>
<p>O <code>uselist=False</code> faz com que <code>pessoa.passaporte</code> retorne um objeto único, não uma lista.</p>
<h2>Sessions: Gerenciando o Ciclo de Vida dos Objetos</h2>
<h3>O que é uma Session</h3>
<p>A Session é fundamental em SQLAlchemy ORM. Ela funciona como um gerenciador de contexto que rastreia mudanças nos objetos e sincroniza com o banco de dados. Você não trabalha diretamente com o banco — tudo passa pela Session.</p>
<pre><code class="language-python">from sqlalchemy.orm import sessionmaker
Cria a fábrica de sessions
SessionLocal = sessionmaker(bind=engine)
Cria uma nova session
session = SessionLocal()
try:
Cria um novo usuário
novo_usuario = Usuario(nome='João Silva', email='joao@example.com')
Adiciona à session (mas ainda não está no banco)
session.add(novo_usuario)
Commita as mudanças (agora vai para o banco)
session.commit()
print(f"Usuário criado com ID: {novo_usuario.id}")
finally:
Sempre fecha a session
session.close()</code></pre>
<p>Entenda o fluxo: você cria objetos Python, adiciona à session, e faz commit para persisti-los no banco. A session funciona como um buffer — você pode fazer múltiplas operações e confirmar tudo de uma vez.</p>
<h3>Operações CRUD</h3>
<p>CRUD significa Create, Read, Update, Delete. Vamos cobrir cada uma com exemplos práticos.</p>
<pre><code class="language-python">from sqlalchemy.orm import sessionmaker
SessionLocal = sessionmaker(bind=engine)
CREATE
def criar_usuario(nome, email):
session = SessionLocal()
try:
usuario = Usuario(nome=nome, email=email)
session.add(usuario)
session.commit()
return usuario
except Exception as e:
session.rollback()
print(f"Erro ao criar: {e}")
finally:
session.close()
READ - Buscar um por ID
def obter_usuario_por_id(user_id):
session = SessionLocal()
try:
usuario = session.query(Usuario).filter(Usuario.id == user_id).first()
return usuario
finally:
session.close()
READ - Buscar vários
def listar_usuarios():
session = SessionLocal()
try:
usuarios = session.query(Usuario).all()
return usuarios
finally:
session.close()
UPDATE
def atualizar_usuario(user_id, novo_nome):
session = SessionLocal()
try:
usuario = session.query(Usuario).filter(Usuario.id == user_id).first()
if usuario:
usuario.nome = novo_nome
session.commit()
return usuario
except Exception as e:
session.rollback()
print(f"Erro ao atualizar: {e}")
finally:
session.close()
DELETE
def deletar_usuario(user_id):
session = SessionLocal()
try:
usuario = session.query(Usuario).filter(Usuario.id == user_id).first()
if usuario:
session.delete(usuario)
session.commit()
except Exception as e:
session.rollback()
print(f"Erro ao deletar: {e}")
finally:
session.close()</code></pre>
<p>Note que sempre usamos <code>try/finally</code> para garantir que a session seja fechada, mesmo com erros. O <code>rollback()</code> desfaz mudanças não commitadas — é essencial para manter consistência.</p>
<h3>Queries Avançadas e Filtering</h3>
<p>SQLAlchemy permite queries expresivas que são traduzidas para SQL.</p>
<pre><code class="language-python">from sqlalchemy import and_, or_
session = SessionLocal()
Filtro simples
usuarios = session.query(Usuario).filter(Usuario.nome == 'João Silva').all()
Múltiplos filtros (AND implícito)
usuarios = session.query(Usuario).filter(
Usuario.nome == 'João Silva',
Usuario.email.like('%@gmail.com')
).all()
OR
usuarios = session.query(Usuario).filter(
or_(
Usuario.nome == 'João',
Usuario.nome == 'Maria'
)
).all()
Ordenação
usuarios = session.query(Usuario).order_by(Usuario.nome).all()
Limite
primeiros_5 = session.query(Usuario).limit(5).all()
Contar
total = session.query(Usuario).count()
Acessando relacionamentos
usuario = session.query(Usuario).filter(Usuario.id == 1).first()
if usuario:
Acessa todos os pedidos do usuário
for pedido in usuario.pedidos:
print(pedido.descricao)
session.close()</code></pre>
<p>O método <code>filter()</code> aceita condições Python que são automaticamente traduzidas para SQL. Operadores como <code>==</code>, <code>.like()</code>, <code>.in_()</code> funcionam intuitivamente.</p>
<h3>Lazy Loading vs Eager Loading</h3>
<p>Por padrão, SQLAlchemy carrega relacionamentos sob demanda (lazy loading). Isso pode causar múltiplas queries. Eager loading carrega tudo de uma vez.</p>
<pre><code class="language-python">from sqlalchemy.orm import joinedload
session = SessionLocal()
Lazy loading (padrão) - vai fazer uma query para cada pedido acessado
usuarios = session.query(Usuario).all()
for usuario in usuarios:
print(usuario.pedidos) # Cada acesso gera uma query
Eager loading - carrega tudo em uma única query
usuarios = session.query(Usuario).options(joinedload(Usuario.pedidos)).all()
for usuario in usuarios:
print(usuario.pedidos) # Já está em memória, sem query extra
session.close()</code></pre>
<p>Eager loading é mais eficiente quando você sabe que vai precisar dos dados relacionados. Use <code>.options(joinedload(...))</code> para carregá-los junto.</p>
<h2>Casos de Uso Prático e Padrões</h2>
<h3>Integração com FastAPI</h3>
<p>Um padrão comum é usar SQLAlchemy com FastAPI. Aqui está uma aplicação funcional completa.</p>
<pre><code class="language-python">from fastapi import FastAPI, Depends
from sqlalchemy import create_engine, Column, Integer, String
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker, Session
from pydantic import BaseModel
Setup
DATABASE_URL = "sqlite:///./test.db"
engine = create_engine(DATABASE_URL, connect_args={"check_same_thread": False})
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
Base = declarative_base()
Model
class Produto(Base):
__tablename__ = "produtos"
id = Column(Integer, primary_key=True)
nome = Column(String(100), nullable=False)
preco = Column(Integer, nullable=False)
Base.metadata.create_all(bind=engine)
Schemas Pydantic
class ProdutoSchema(BaseModel):
id: int
nome: str
preco: int
class Config:
from_attributes = True
class ProdutoCreate(BaseModel):
nome: str
preco: int
Dependency para injetar session
def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()
API
app = FastAPI()
@app.post("/produtos/", response_model=ProdutoSchema)
def criar_produto(produto: ProdutoCreate, db: Session = Depends(get_db)):
novo = Produto(nome=produto.nome, preco=produto.preco)
db.add(novo)
db.commit()
db.refresh(novo)
return novo
@app.get("/produtos/{produto_id}", response_model=ProdutoSchema)
def obter_produto(produto_id: int, db: Session = Depends(get_db)):
return db.query(Produto).filter(Produto.id == produto_id).first()
@app.get("/produtos/", response_model=list[ProdutoSchema])
def listar_produtos(db: Session = Depends(get_db)):
return db.query(Produto).all()</code></pre>
<p>O padrão de dependency injection (<code>Depends(get_db)</code>) fornece uma session limpa para cada requisição e a fecha automaticamente.</p>
<h3>Tratamento de Erros e Validação</h3>
<p>Erros de banco de dados devem ser tratados com cuidado. SQLAlchemy lança exceções que você deve antecipar.</p>
<pre><code class="language-python">from sqlalchemy.exc import IntegrityError, SQLAlchemyError
session = SessionLocal()
try:
Email duplicado vai gerar IntegrityError
usuario1 = Usuario(nome='Ana', email='ana@test.com')
usuario2 = Usuario(nome='Bruno', email='ana@test.com') # Mesmo email
session.add(usuario1)
session.add(usuario2)
session.commit()
except IntegrityError as e:
session.rollback()
print("Email já existe no banco!")
except SQLAlchemyError as e:
session.rollback()
print(f"Erro genérico de banco: {e}")
finally:
session.close()</code></pre>
<p><code>IntegrityError</code> captura violações de constraints (unique, foreign key, etc). <code>SQLAlchemyError</code> é a classe mãe de todos os erros SQLAlchemy.</p>
<h2>Conclusão</h2>
<p>Os três pilares que você deve levar deste artigo são:</p>
<ol>
<li><strong>Models são sua ponte com o banco de dados</strong> — cada classe herda de Base e mapeia para uma tabela. Defina tipos corretos, restrições claras, e os relacionamentos com precisão. Isso previne bugs e garante integridade dos dados.</li>
</ol>
<ol>
<li><strong>Relacionamentos exigem planejamento</strong> — One-to-Many, Many-to-Many, One-to-One têm usos distintos. Entenda quando usar cada um, porque isso afeta como você consulta os dados e como o banco os organiza. Use <code>back_populates</code> para manter referências bidirecionais limpas.</li>
</ol>
<ol>
<li><strong>Sessions são o coração da ORM</strong> — toda interação com o banco passa por ela. Sempre feche sessions em finally, use commit() para persisti-las, e rollback() para desfazer mudanças. Eager loading com joinedload previne queries N+1 desnecessárias.</li>
</ol>
<h2>Referências</h2>
<ul>
<li><a href="https://docs.sqlalchemy.org/en/20/orm/" target="_blank" rel="noopener noreferrer">Documentação Oficial SQLAlchemy ORM</a></li>
<li><a href="https://fastapi.tiangolo.com/tutorial/sql-databases/" target="_blank" rel="noopener noreferrer">FastAPI + SQLAlchemy - Documentação FastAPI</a></li>
<li><a href="https://realpython.com/sqlalchemy-tutorial/" target="_blank" rel="noopener noreferrer">Real Python: SQLAlchemy ORM Tutorial</a></li>
<li><a href="https://docs.sqlalchemy.org/en/20/orm/basic_relationships.html" target="_blank" rel="noopener noreferrer">SQLAlchemy Relationships Guide</a></li>
<li><a href="https://blog.miguelgrinberg.com/post/the-application-factory-pattern" target="_blank" rel="noopener noreferrer">Miguel Grinberg's Flask-SQLAlchemy Pattern</a></li>
</ul>
<p><!-- FIM --></p>