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 Pedidos de 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 Joins para coleções 1:N? Um LEFT JOIN entre 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 com ON 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 Usuario tem um campo Pedidos []Pedido e a struct Pedido tem um campo Usuario 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.