Python

Boas Práticas de SQLAlchemy ORM em Python: Models, Relacionamentos e Sessions para Times Ágeis

15 min de leitura

Boas Práticas de SQLAlchemy ORM em Python: Models, Relacionamentos e Sessions para Times Ágeis

O que é SQLAlchemy ORM e por que você precisa dela 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. 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. Models: Definindo suas Entidades Estrutura Básica de um Model Um Model é uma classe Python que representa uma tabela no banco de dados. Cada atributo da classe é uma coluna, e cada instância

<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__ = &#039;usuarios&#039;

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(&#039;sqlite:///banco.db&#039;)

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 = &quot;pendente&quot;

PROCESSANDO = &quot;processando&quot;

CONCLUIDO = &quot;concluido&quot;

class Pedido(Base):

__tablename__ = &#039;pedidos&#039;

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__ = &#039;usuarios&#039;

id = Column(Integer, primary_key=True)

nome = Column(String(100), nullable=False)

Aqui vinculamos os pedidos do usuário

pedidos = relationship(&#039;Pedido&#039;, back_populates=&#039;usuario&#039;)

class Pedido(Base):

__tablename__ = &#039;pedidos&#039;

id = Column(Integer, primary_key=True)

descricao = Column(String(255), nullable=False)

usuario_id = Column(Integer, ForeignKey(&#039;usuarios.id&#039;), nullable=False)

Aqui referenciamos o usuário

usuario = relationship(&#039;Usuario&#039;, back_populates=&#039;pedidos&#039;)</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(

&#039;aluno_curso&#039;,

Base.metadata,

Column(&#039;aluno_id&#039;, Integer, ForeignKey(&#039;alunos.id&#039;), primary_key=True),

Column(&#039;curso_id&#039;, Integer, ForeignKey(&#039;cursos.id&#039;), primary_key=True)

)

class Aluno(Base):

__tablename__ = &#039;alunos&#039;

id = Column(Integer, primary_key=True)

nome = Column(String(100), nullable=False)

cursos = relationship(&#039;Curso&#039;, secondary=aluno_curso, back_populates=&#039;alunos&#039;)

class Curso(Base):

__tablename__ = &#039;cursos&#039;

id = Column(Integer, primary_key=True)

titulo = Column(String(150), nullable=False)

alunos = relationship(&#039;Aluno&#039;, secondary=aluno_curso, back_populates=&#039;cursos&#039;)</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__ = &#039;pessoas&#039;

id = Column(Integer, primary_key=True)

nome = Column(String(100), nullable=False)

passaporte = relationship(&#039;Passaporte&#039;, uselist=False, back_populates=&#039;pessoa&#039;)

class Passaporte(Base):

__tablename__ = &#039;passaportes&#039;

id = Column(Integer, primary_key=True)

numero = Column(String(20), unique=True, nullable=False)

pessoa_id = Column(Integer, ForeignKey(&#039;pessoas.id&#039;), unique=True)

pessoa = relationship(&#039;Pessoa&#039;, back_populates=&#039;passaporte&#039;)</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=&#039;João Silva&#039;, email=&#039;joao@example.com&#039;)

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&quot;Usuário criado com ID: {novo_usuario.id}&quot;)

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&quot;Erro ao criar: {e}&quot;)

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&quot;Erro ao atualizar: {e}&quot;)

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&quot;Erro ao deletar: {e}&quot;)

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 == &#039;João Silva&#039;).all()

Múltiplos filtros (AND implícito)

usuarios = session.query(Usuario).filter(

Usuario.nome == &#039;João Silva&#039;,

Usuario.email.like(&#039;%@gmail.com&#039;)

).all()

OR

usuarios = session.query(Usuario).filter(

or_(

Usuario.nome == &#039;João&#039;,

Usuario.nome == &#039;Maria&#039;

)

).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 = &quot;sqlite:///./test.db&quot;

engine = create_engine(DATABASE_URL, connect_args={&quot;check_same_thread&quot;: False})

SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)

Base = declarative_base()

Model

class Produto(Base):

__tablename__ = &quot;produtos&quot;

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(&quot;/produtos/&quot;, 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(&quot;/produtos/{produto_id}&quot;, 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(&quot;/produtos/&quot;, 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=&#039;Ana&#039;, email=&#039;ana@test.com&#039;)

usuario2 = Usuario(nome=&#039;Bruno&#039;, email=&#039;ana@test.com&#039;) # Mesmo email

session.add(usuario1)

session.add(usuario2)

session.commit()

except IntegrityError as e:

session.rollback()

print(&quot;Email já existe no banco!&quot;)

except SQLAlchemyError as e:

session.rollback()

print(f&quot;Erro genérico de banco: {e}&quot;)

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&#039;s Flask-SQLAlchemy Pattern</a></li>

</ul>

<p>&lt;!-- FIM --&gt;</p>

Comentários

Mais em Python

Dominando asyncio Avançado em Python: Semáforos, Locks e Padrões de Concorrência em Projetos Reais
Dominando asyncio Avançado em Python: Semáforos, Locks e Padrões de Concorrência em Projetos Reais

Introdução: O Problema da Concorrência Controlada Quando trabalhamos com em P...

Variáveis, Tipos Primitivos e Tipagem Dinâmica em Python na Prática
Variáveis, Tipos Primitivos e Tipagem Dinâmica em Python na Prática

O Que São Variáveis em Python Uma variável é um espaço na memória do computad...

Boas Práticas de Dataclasses em Python: @dataclass, fields e post_init para Times Ágeis
Boas Práticas de Dataclasses em Python: @dataclass, fields e post_init para Times Ágeis

O que são Dataclasses e Por Que Importam Dataclasses são uma ferramenta poder...