Capítulo 07: Relacionamentos entre Modelos e Consultas com Preload
Especialização em Backend com Go & Gin • Gin Framework & Go 1.22+ • GORM, Clean Architecture, Concorrência e Alta Performance
🗺️ Mapa Conceitual do Tópico
flowchart TD
A["Entidade Usuário (1)"] -->|"1 : N (Has Many)"| B["Entidade Pedido (N)"]
B -->|"N : M (Many to Many)"| C["Entidade Produto (M)"]
subgraph ASSOCIACAO["Tabela Associativa"]
D["item_pedidos (pedido_id, produto_id)"]
end
B <--> D <--> C
subgraph CONSULTA_PRELOAD["Estratégias de Carregamento de Relacionamentos"]
E["db.Preload('Pedidos')"] --> F["Executa Query Principal + Query com WHERE usuario_id IN (...)"]
G["db.Joins('Perfil')"] --> H["Executa 1 Única Query via INNER / LEFT JOIN"]
I["db.Preload('Pedidos.Itens.Produto')"] --> J["Preloading Profundo Aninhado Multinível"]
end
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:#fce4ec,stroke:#e91e63,stroke-width:2px
style E fill:#e8f5e9,stroke:#4caf50,stroke-width:2px
style F fill:#e0f2f1,stroke:#009688,stroke-width:2px
style G fill:#fffde7,stroke:#fbc02d,stroke-width:2px
style H fill:#e0f2f1,stroke:#009688,stroke-width:2px
style I fill:#f3e5f5,stroke:#9c27b0,stroke-width:2px
style J fill:#c8e6c9,stroke:#2e7d32,stroke-width:2px
🏛️ 1. As Quatro Associações Fundamentais do GORM
O GORM suporta nativamente todas as formas de relacionamento relacional, configuradas via convenção de nomenclatura ou anotações declarativas em tags:
| Tipo de Associação | Definição na Struct Go | Exemplo Prático | Convenção de Chave Estrangeira |
|---|---|---|---|
Belongs To |
Campo com struct e chave estrangeira | Usuario Usuario gorm:"foreignKey:UsuarioID" |
O modelo filho armazena o ID do pai (UsuarioID). |
Has One |
Campo único do tipo struct | Perfil Perfil gorm:"foreignKey:UsuarioID" |
O modelo associado possui o ID do modelo atual. |
Has Many |
Slice de structs ([]Filho) |
Pedidos []Pedido gorm:"foreignKey:ClienteID" |
O modelo filho armazena ClienteID. |
Many To Many |
Slice com tag many2many |
Papeis []Papel gorm:"many2many:usuario_papeis" |
O GORM gerencia a tabela intermediária associativa. |
package models
import "gorm.io/gorm"
type Usuario struct {
gorm.Model
Nome string `gorm:"size:120;not null" json:"nome"`
Email string `gorm:"size:120;uniqueIndex;not null" json:"email"`
Perfil Perfil `gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE;" json:"perfil"`
Pedidos []Pedido `gorm:"foreignKey:UsuarioID" json:"pedidos"`
Papeis []Papel `gorm:"many2many:usuario_papeis;" json:"papeis"`
}
type Perfil struct {
gorm.Model
UsuarioID uint `gorm:"uniqueIndex;not null" json:"usuario_id"`
Bio string `gorm:"size:500" json:"bio"`
AvatarURL string `gorm:"size:255" json:"avatar_url"`
}
type Pedido struct {
gorm.Model
UsuarioID uint `gorm:"index;not null" json:"usuario_id"`
ValorTotal float64 `gorm:"type:decimal(10,2);not null" json:"valor_total"`
Status string `gorm:"size:30;default:'PENDENTE'" json:"status"`
}
type Papel struct {
gorm.Model
Nome string `gorm:"size:50;uniqueIndex;not null" json:"nome"`
}
⚡ 2. O Problema das N+1 Consultas e a Solução Preload
Em sistemas web com bancos relacionais, o antipadrão mais comum de latência é o Problema das N+1 Consultas: buscar uma lista de 50 usuários e, em um loop subsequente, disparar 50 queries individuais para carregar os pedidos de cada um. Isso gera 51 chamadas de rede ao banco de dados para atender uma única requisição HTTP.
O GORM resolve esse problema através do método Preload (Eager Loading):
- O GORM executa a query principal para obter os 50 usuários (
SELECT * FROM usuarios LIMIT 50). - Coleta todos os 50 IDs obtidos.
- Executa uma única segunda consulta otimizada usando a cláusula
WHERE usuario_id IN (1, 2, ..., 50). - Preenche os slices
Pedidosde cada struct em memória Go de forma transparente.
package repository
import (
"gorm.io/gorm"
"meu-modulo/models"
)
func ObterUsuariosCompletos(db *gorm.DB) ([]models.Usuario, error) {
var usuarios []models.Usuario
// Carrega Perfil (1:1), Pedidos (1:N) e Papeis (N:M) em apenas 4 queries otimizadas
err := db.
Preload("Perfil").
Preload("Pedidos", "status != ?", "CANCELADO"). // Preload com filtro condicional
Preload("Papeis").
Find(&usuarios).Error
return usuarios, err
}
🔍 3. Joins() vs Preload(): Quando Utilizar Cada Um
O GORM oferece duas formas distintas de carregar dados relacionados:
// 1. Joins: Executa 1 única query SQL via LEFT/INNER JOIN
// Recomendado para relações 1:1 (Has One) e N:1 (Belongs To)
var perfis []models.Usuario
db.Joins("Perfil").Find(&perfis)
// 2. Preload: Executa consultas separadas com clausula IN (...)
// Recomendado e OBRIGATÓRIO para coleções 1:N (Has Many) e N:M (Many to Many)
var usuariosComPedidos []models.Usuario
db.Preload("Pedidos").Find(&usuariosComPedidos)
Por que não usar
Joinspara coleções 1:N? UmLEFT JOINentre Usuários e Pedidos multiplica as linhas do lado pai. Se um usuário tiver 10 pedidos, os dados do usuário serão duplicados 10 vezes no tráfego da rede, consumindo memória excessiva e exigindo agrupamento complexo no cliente.
🛡️ 4. Cascatas de Deleção e Prevenção de Ciclos JSON
- Constraint DDL no Banco: A anotação
constraint:OnUpdate:CASCADE,OnDelete:CASCADE;instrui o PostgreSQL/MySQL a criar a restrição física de chave estrangeira comON DELETE CASCADE. Quando o usuário é excluído, o banco remove os registros órfãos automaticamente. - Prevenção de Ciclo Infinito de Serialização: Se a struct
Usuariotem um campoPedidos []Pedidoe a structPedidotem um campoUsuario Usuario, o serializador padrão do Gin (c.JSON(200, usuario)) entrará em um loop infinito de recursão até estourar a pilha (stack overflow). - A Solução Idiomática: Adicionar a tag
json:"-"no campo inverso de volta (Usuario *Usuario json:"-"), ou melhor ainda: definir DTOs de Resposta Específicos que contêm apenas as propriedades necessárias para a resposta do cliente.