Capítulo 07: Relacionamentos entre Tabelas (Relationship) e Consultas com Join
Especialização em Backend com Python & FastAPI • FastAPI & Python 3.12+ • Pydantic v2, SQLModel, Async/Await e Microsserviços
🗺️ Mapa Conceitual do Tópico
flowchart TD
A["Cliente HTTP / Frontend"] --> B["API Gateway / Router"]
B --> C["Controller / Handler"]
C --> D["Service Layer (Regras de Negócio)"]
D --> E["Repository / ORM (Persistência)"]
E --> F["Banco de Dados / Cache"]
subgraph ARQ["Arquitetura do Capítulo"]
G["Conceito: Relacionamentos entre Tabelas (Relationship) e Consultas com Join"]
H["Segurança, Validação e Resiliência"]
I["Alta Performance e Escalabilidade"]
end
D --> ARQ
style A fill:#e1f5fe,stroke:#03a9f4,stroke-width:2px
style B fill:#fff3e0,stroke:#ff9800,stroke-width:2px
style C fill:#ede7f6,stroke:#7e57c2,stroke-width:2px
style D fill:#e8f5e9,stroke:#4caf50,stroke-width:2px
style E fill:#fce4ec,stroke:#e91e63,stroke-width:2px
style F fill:#f3e5f5,stroke:#9c27b0,stroke-width:2px
🏛️ 1. Foreign Key vs. Relationship: dois conceitos distintos
Em SQLModel, a foreign key é a coluna real do banco que garante integridade referencial no nível SQL — sem ela, o banco aceitaria um Livro apontando para um Autor inexistente. Já Relationship() é um atributo puramente do lado Python/SQLAlchemy: não gera coluna nem existe no SELECT a menos que seja explicitamente navegado; ele instrui o ORM sobre como carregar objetos relacionados em memória.
from typing import List, Optional
from sqlmodel import SQLModel, Field, Relationship
class Autor(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
nome: str
livros: List["Livro"] = Relationship(back_populates="autor")
class Livro(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
titulo: str
autor_id: int = Field(foreign_key="autor.id")
autor: Optional[Autor] = Relationship(back_populates="livros")
back_populates mantém os dois lados sincronizados em memória: atribuir livro.autor = a também atualiza a.livros automaticamente, sem consultas adicionais, desde que os objetos já estejam carregados na mesma sessão.
Muitos-para-muitos com tabela de ligação
Relacionamentos N:N exigem uma tabela intermediária explícita — SQLModel não a gera magicamente:
class UsuarioPerfilLink(SQLModel, table=True):
usuario_id: Optional[int] = Field(default=None, foreign_key="usuario.id", primary_key=True)
perfil_id: Optional[int] = Field(default=None, foreign_key="perfil.id", primary_key=True)
class Perfil(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
nome: str
usuarios: List["Usuario"] = Relationship(back_populates="perfis", link_model=UsuarioPerfilLink)
class Usuario(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
nome: str
perfis: List[Perfil] = Relationship(back_populates="usuarios", link_model=UsuarioPerfilLink)
A tabela de ligação tem chave primária composta pelas duas foreign keys — cada linha representa uma associação, e o par (usuario_id, perfil_id) nunca se repete.
O problema N+1 e como evitá-lo
Acessar autor.livros de forma “preguiçosa” (lazy loading) dentro de um loop — por exemplo, ao serializar uma lista de 50 autores e, para cada um, ler .livros — dispara uma consulta SQL adicional por autor: 1 consulta inicial + N consultas subsequentes. Em contexto assíncrono isso é ainda mais grave, porque o lazy loading implícito não funciona de forma transparente com AsyncSession (pode lançar MissingGreenlet) e precisa ser evitado deliberadamente com eager loading:
from sqlalchemy.orm import selectinload
from sqlmodel import select
async def listar_autores_com_livros(session):
stmt = select(Autor).options(selectinload(Autor.livros))
resultado = await session.exec(stmt)
return resultado.all()
selectinload executa uma segunda consulta única com WHERE autor_id IN (...) para todos os autores retornados — 2 consultas no total, independente de N. A alternativa joinedload faz tudo em uma única consulta com LEFT OUTER JOIN, mas duplica linhas do lado “um” quando a relação “muitos” tem múltiplos registros, o que pode ser menos eficiente para coleções grandes. Para junções explícitas e projeções tabulares (não navegação de objetos), usa-se .join() diretamente na consulta:
stmt = (
select(Autor.nome, Livro.titulo)
.join(Livro, Livro.autor_id == Autor.id)
)
🔗 Recursos Pedagógicos do Capítulo 07
| Recurso Didático | Finalidade | Link de Acesso |
|---|---|---|
| 📊 Slides de Aula | Apresentação visual interativa com Dark Mode e suporte a teclado | Ver Slides |
| 🧠 Quiz Formativo | Teste interativo de fixação com feedback imediato por alternativa | Fazer Quiz |
| 💻 Exemplos de Código | Demonstrações funcionais com código executável | Ver Exemplos |
| 🧩 Exercícios em 4 Níveis | Lista progressiva de fixação com gabarito em bloco colapsável | Resolver Exercícios |
| ⬅️ Capítulo Anterior | 📚 Sumário de Tópicos | Próximo Capítulo ➡️ |