📚 Módulo 01: Modelagem e Banco de Dados (ER e SQLAlchemy)
Em disciplinas de Banco de Dados, o Mapeamento Objeto-Relacional (ORM) é a ponte que une o modelo conceitual (classes orientadas a objetos) ao modelo físico (tabelas e chaves estrangeiras em banco relacional).
Neste módulo, modelaremos a TecLoja 02 usando diagramas em Mermaid e escreveremos os códigos das entidades usando o estilo declarativo moderno do SQLAlchemy 2.0 com anotações de tipo estáticas de Python.
📊 1. Diagrama de Entidade-Relacionamento (ERD)
O banco de dados do nosso e-commerce possui relações 1:N (uma Categoria tem vários Produtos; um Cliente faz vários Pedidos) e uma relação N:M (Pedidos e Produtos se relacionam através de uma tabela associativa ItemPedido contendo atributos históricos extras).
erDiagram
CATEGORIAS ||--o{ PRODUTOS : "1:N"
CLIENTES ||--o{ PEDIDOS : "1:N"
PEDIDOS ||--o{ ITENS_PEDIDO : "1:N (Composto)"
PRODUTOS ||--o{ ITENS_PEDIDO : "1:N (Composto)"
USUARIOS ||--o{ PAPEIS : "N:M (Simplificado)"
CATEGORIAS {
bigint id PK
varchar nome
}
PRODUTOS {
bigint id PK
varchar nome
varchar descricao
numeric preco
integer estoque
bigint categoria_id FK
}
CLIENTES {
bigint id PK
varchar nome
varchar email
}
PEDIDOS {
bigint id PK
timestamp data_pedido
varchar status
bigint cliente_id FK
}
ITENS_PEDIDO {
bigint id PK
bigint pedido_id FK
bigint produto_id FK
integer quantidade
numeric preco_unitario
}
USUARIOS {
bigint id PK
varchar username
varchar password_hash
varchar papel
}
🛠️ 2. Mapeamento Declarativo Moderno com SQLAlchemy 2.0
O SQLAlchemy 2.0 introduziu anotações de tipo estático de Python (Mapped e mapped_column). Isso permite autocompletar e detectar erros de compilação/digitação estaticamente nas IDEs de desenvolvimento.
Primeiro, definimos a nossa classe base declarativa comum em app/models/base.py:
from sqlalchemy.orm import DeclarativeBase
class Base(DeclarativeBase):
pass
1. Entidade Categoria e Produto (Relação 1:N)
Crie o arquivo em app/models/produto.py:
from typing import List, Optional
from decimal import Decimal
from sqlalchemy import ForeignKey, String, Numeric, Integer
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.models.base import Base
class Categoria(Base):
__tablename__ = "categorias"
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
nome: Mapped[str] = mapped_column(String(100), nullable=False)
# Relação bidirecional (uma categoria tem vários produtos)
produtos: Mapped[List["Produto"]] = relationship(
back_populates="categoria",
cascade="all, delete-orphan"
)
class Produto(Base):
__tablename__ = "produtos"
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
nome: Mapped[str] = mapped_column(String(150), nullable=False)
descricao: Mapped[Optional[str]] = mapped_column(String(500))
preco: Mapped[Decimal] = mapped_column(Numeric(10, 2), nullable=False)
estoque: Mapped[int] = mapped_column(Integer, default=0)
# Chave estrangeira física no banco
categoria_id: Mapped[int] = mapped_column(ForeignKey("categorias.id"), nullable=False)
# Relações carregadas preguiçosamente (lazy load por padrão)
categoria: Mapped["Categoria"] = relationship(back_populates="produtos")
2. Entidade Cliente, Pedido e ItemPedido (Relação N:M Complexa)
Em banco de dados, quando a relação N:M possui atributos extras na interseção (como a quantidade e o preço unitário histórico cobrado no ato da compra), a boa prática é mapear a tabela de junção fisicamente como uma classe dedicada (ItemPedido).
Crie o arquivo em app/models/pedido.py:
from datetime import datetime
from decimal import Decimal
from typing import List
from sqlalchemy import ForeignKey, Numeric, Integer, String, DateTime
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.models.base import Base
from app.models.produto import Produto
class Cliente(Base):
__tablename__ = "clientes"
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
nome: Mapped[str] = mapped_column(String(100), nullable=False)
email: Mapped[str] = mapped_column(String(100), unique=True, nullable=False)
pedidos: Mapped[List["Pedido"]] = relationship(back_populates="cliente")
class Pedido(Base):
__tablename__ = "pedidos"
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
data_pedido: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
status: Mapped[str] = mapped_column(String(50), default="PAGO")
cliente_id: Mapped[int] = mapped_column(ForeignKey("clientes.id"), nullable=False)
cliente: Mapped["Cliente"] = relationship(back_populates="pedidos")
# Relação com cascata completa (ao apagar um pedido, apaga todos os seus itens associados)
itens: Mapped[List["ItemPedido"]] = relationship(
back_populates="pedido",
cascade="all, delete-orphan",
lazy="selectin" # Otimiza o carregamento dos itens de forma segura em async
)
class ItemPedido(Base):
__tablename__ = "itens_pedido"
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
pedido_id: Mapped[int] = mapped_column(ForeignKey("pedidos.id"), nullable=False)
produto_id: Mapped[int] = mapped_column(ForeignKey("produtos.id"), nullable=False)
quantidade: Mapped[int] = mapped_column(Integer, nullable=False)
preco_unitario: Mapped[Decimal] = mapped_column(Numeric(10, 2), nullable=False)
pedido: Mapped["Pedido"] = relationship(back_populates="itens")
# Carregamento antecipado (Eager Loading) para evitar N+1 query problem ao ler produtos
produto: Mapped["Produto"] = relationship(lazy="joined")
3. Entidade Usuário e Controle de Acesso (Segurança)
Para blindar a API, criaremos uma tabela simplificada para gerenciar as credenciais e as permissões de acesso (papéis de ADMIN e USER).
Crie o arquivo em app/models/usuario.py:
from sqlalchemy import String
from sqlalchemy.orm import Mapped, mapped_column
from app.models.base import Base
class Usuario(Base):
__tablename__ = "usuarios"
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
username: Mapped[str] = mapped_column(String(50), unique=True, nullable=False)
password_hash: Mapped[str] = mapped_column(String(255), nullable=False)
# Papel/Role do usuário (ex: 'ROLE_ADMIN' ou 'ROLE_USER')
papel: Mapped[str] = mapped_column(String(50), default="ROLE_USER")
🧠 3. Conceito Importante: Carregamento Lazy vs Eager (Evitando Queries N+1)
Nas aulas de Banco de Dados, é fundamental ensinar aos alunos como o ORM se comporta ao carregar dados relacionados:
- Lazy Loading (Carregamento Preguiçoso): O SQLAlchemy não busca a categoria de um produto no banco até que você faça explicitamente
produto.categoria.nome. No modo assíncrono do Python (async/await), tentar fazer isso sem carregar previamente gerará uma exceção de erro assíncrono. - Eager Loading (Carregamento Antecipado):
lazy="joined": Realiza umLEFT OUTER JOINSQL na própria consulta original, trazendo o registro relacionado de forma imediata (usado em relações 1:1 ou N:1 comoItemPedido -> Produto).lazy="selectin": Realiza um segundoSELECTutilizando a cláusulaIN (ids)para buscar coleções relacionadas de forma ultraeficiente, sem causar duplicação de linhas causada por múltiplos JOINS (ideal para relações 1:N comoPedido -> ItemPedido).
✅ Pré-Requisitos deste Módulo
Antes de começar a escrever o código das entidades, certifique-se de que:
- A pasta do projeto backend (
tecloja-backend) foi criada localmente. - A pasta de código da aplicação
appe a subpastamodelsforam estruturadas sobtecloja-backend/app/models/.
🤔 Por que fizemos assim?
- Por que SQLAlchemy 2.0 com
Mapped[]? O padrão antigo do SQLAlchemy (usandoColumn(...)) não fornecia tipagem estática nativa. Ao adotarMapped[int] = mapped_column(), as IDEs conseguem fazer checagem estática completa do tipo de dado (Python type-hinting), autocompletar atributos e evitar erros de digitação de colunas. - Por que mapear
ItemPedidocomo uma classe dedicada em vez de uma tabela secundária invisível? Em relações N:M simples, uma tabela associativa sem atributos extras pode ser oculta pelo ORM. No entanto, no e-commerce, o carrinho e o pedido precisam congelar a quantidade e o preço unitário no ato da compra. Se o preço do produto for alterado no catálogo no dia seguinte, o histórico de faturamento não pode mudar. Por isso, a tabela de junção vira uma entidade explícita no mapeamento. - Por que gerenciar carregamentos de relacionamento (
lazy)? O acesso assíncrono impede que o ORM faça requisições automáticas bloqueantes ao banco (o comportamento padrão do Lazy Loading). Por isso, no async definimos carregamentos explícitos na inicialização:selectin(ótimo para coleções como itens de um pedido) ejoined(ótimo para carregar a entidade um-para-um, como o produto dentro de cada item).
🔍 Checkpoint
- Arquivos Criados: Garanta que os seguintes arquivos foram criados na pasta correta:
tecloja-backend/app/models/base.pytecloja-backend/app/models/produto.pytecloja-backend/app/models/pedido.pytecloja-backend/app/models/usuario.py
- Sintaxe de Importação: Verifique se as classes importam a classe
Basedo arquivo comumbase.pypara sincronizar os metadados do ORM.
⚠️ Erros Comuns
| Erro | Causa | Solução |
|---|---|---|
sqlalchemy.exc.InvalidRequestError: Table '...' is already defined |
Importações circulares de módulos ou redeclaração da mesma tabela com o mesmo __tablename__. |
Certifique-se de usar referências em string nos relacionamentos (ex: "Produto" em vez da classe importada) para evitar dependências circulares de arquivos. |
MissingGreenlet: Instance <...> is not bound to a Session |
Tentativa de acessar um relacionamento preguiçoso (Lazy Load) em um contexto assíncrono. | Em conexões assíncronas do SQLAlchemy, configure a propriedade lazy="selectin" ou lazy="joined" nas relações do model, ou carregue-as explicitamente via selectinload / joinedload na query. |
🏁 Conclusão
Sensacional! Toda a modelagem conceitual e o mapeamento relacional físico da nossa base de dados estão definidos usando o padrão declarativo avançado do SQLAlchemy 2.0.
No Módulo 02, faremos o setup das dependências do projeto Python em requirements.txt, inicializaremos o Alembic para versionar a evolução das tabelas criadas e escreveremos um script Data Seeder completo para carregar eletrônicos fictícios em nossa base de dados local!