📚 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:

  1. 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.
  2. Eager Loading (Carregamento Antecipado):
    • lazy="joined": Realiza um LEFT OUTER JOIN SQL na própria consulta original, trazendo o registro relacionado de forma imediata (usado em relações 1:1 ou N:1 como ItemPedido -> Produto).
    • lazy="selectin": Realiza um segundo SELECT utilizando a cláusula IN (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 como Pedido -> ItemPedido).

✅ Pré-Requisitos deste Módulo

Antes de começar a escrever o código das entidades, certifique-se de que:


🤔 Por que fizemos assim?


🔍 Checkpoint

  1. Arquivos Criados: Garanta que os seguintes arquivos foram criados na pasta correta:
    • tecloja-backend/app/models/base.py
    • tecloja-backend/app/models/produto.py
    • tecloja-backend/app/models/pedido.py
    • tecloja-backend/app/models/usuario.py
  2. Sintaxe de Importação: Verifique se as classes importam a classe Base do arquivo comum base.py para 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!


Voltar para o Sumário