Capítulo 07: Relacionamentos Eloquent (1:1, 1:N, N:N) e Eager Loading
Especialização em Backend com PHP & Laravel • Laravel 11 & PHP 8.3+ • Eloquent ORM, Sanctum, Filas, Horizon e APIs Corporativas
🗺️ Mapa Conceitual do Tópico
flowchart TD
A["Cliente hasMany Pedido"] --> B["Pedido belongsTo Cliente"]
B --> C["Pedido belongsToMany Produto (pivot: item_pedidos)"]
C --> D["Sem with(): 1 query + N queries (N+1)"]
C --> E["Com with()/load(): 2 ou 3 queries otimizadas"]
subgraph OPT["Otimizações"]
F["withCount() — contagem sem carregar modelos"]
G["Model::preventLazyLoading() em dev"]
end
E --> OPT
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:#ffebee,stroke:#f44336,stroke-width:2px
style E fill:#e8f5e9,stroke:#4caf50,stroke-width:2px
🏛️ 1. Os Três Tipos de Relacionamento
O Eloquent expressa relacionamentos como métodos no modelo, não como propriedades — cada chamada retorna um objeto Relation que só é convertido em consulta quando efetivamente acessado:
<?php
// app/Models/Cliente.php
public function pedidos(): HasMany
{
return $this->hasMany(Pedido::class); // 1 Cliente : N Pedidos
}
<?php
// app/Models/Pedido.php
public function cliente(): BelongsTo
{
return $this->belongsTo(Cliente::class); // inverso do hasMany
}
public function produtos(): BelongsToMany
{
return $this->belongsToMany(Produto::class, 'item_pedidos')
->withPivot('quantidade', 'preco_unitario')
->withTimestamps(); // N:N via tabela pivô
}
Na relação N:N, os dados extras da tabela intermediária (item_pedidos) ficam acessíveis via $produto->pivot->quantidade, e withTimestamps() garante que created_at/updated_at da tabela pivô sejam mantidos automaticamente.
⚠️ 2. O Problema N+1 e Como Eliminá-lo
Acessar um relacionamento fora de um foreach já carregado dispara Lazy Loading: uma query nova por iteração. Listar 100 pedidos e acessar $pedido->cliente->nome dentro do loop gera 1 query inicial + 100 queries adicionais — o clássico problema N+1.
// ❌ N+1: 1 query para pedidos + 1 query por cliente acessado no loop
foreach (Pedido::all() as $pedido) {
echo $pedido->cliente->nome;
}
// ✅ Eager Loading: exatamente 2 queries, independente do volume
$pedidos = Pedido::with(['cliente', 'itens'])->paginate(15);
with() pré-carrega o relacionamento em uma consulta separada (WHERE cliente_id IN (...)) e o Eloquent faz o hydration em memória — o acesso subsequente a $pedido->cliente não dispara SQL novo. Quando o modelo já foi buscado e o relacionamento precisa ser carregado depois, usa-se load() em vez de with() (que é usado na query inicial).
Em desenvolvimento, é boa prática travar lazy loading acidental:
// AppServiceProvider::boot()
Model::preventLazyLoading(!app()->isProduction());
Isso lança uma exceção assim que qualquer relacionamento é acessado sem ter sido eager-loaded, expondo o bug antes de chegar em produção — em vez de silenciosamente degradar a performance sob carga real.
📊 3. Agregações sem Carregar os Modelos: withCount()
Quando só o número de registros relacionados importa (ex.: badge “5 pedidos” na listagem de clientes), carregar as coleções inteiras é desperdício. withCount() gera uma subquery SQL de contagem, populando um atributo {relacionamento}_count:
$clientes = Cliente::withCount('pedidos')
->having('pedidos_count', '>', 5)
->get();
// $cliente->pedidos_count — inteiro, sem nenhum Pedido carregado em memória
O mesmo padrão existe para withSum(), withAvg(), withMax() e withMin(), todos resolvidos em uma única consulta agregada.