Associações do Eloquent do Laravel
Relacionamentos são o "superpoder" do Eloquent—com apenas uma linha de código, você pode conectar tabelas usando chaves estrangeiras e se despedir dos JOINs manuais.
1. O Que Você Vai Aprender
- Um-para-um: hasOne/belongsTo
- Um-para-muitos: hasMany/belongsTo
- Muitos-para-muitos: belongsToMany (incluindo tabela pivô)
- HasManyThrough: Associações Remotas e Associações Polimórficas
- Pré-carregamento e Carregamento Preguiçoso: O Problema N+1 e Otimização com
with()/load()
2. Uma História Real de uma Equipe de Análise de Dados
(1) Problema: 50 consultas apenas para exibir 10 pedidos
Charlie vê 10 pedidos no painel do ShopMetrics—ele executa uma consulta para recuperar os pedidos, depois para cada pedido, executa uma consulta para o usuário, uma para a loja e uma para o produto, totalizando 1 + 10 x 4 = 41 consultas SQL. A carga do banco de dados disparou, e o tempo de carregamento da página passou de 200 ms para 3 s. Bob adicionou mais joins (pedido → produto → categoria → tag), levando o número de consultas para mais de 200, e Alice reclamou que o sistema estava "lento como uma lesma."
(2) Solução com Pré-carregamento
O pré-carregamento with() do Eloquent recupera todos os dados associados em uma única consulta, reduzindo de 41 instruções SQL para 4.
// Problema N+1 — 41 consultas
$orders = Order::take(10)->get();
foreach ($orders as $order) {
echo $order->user->name; // +1 consulta cada
echo $order->shop->name; // +1 consulta cada
}
// Eager loading — 4 consultas total
$orders = Order::with(['user', 'shop', 'items.product'])->take(10)->get();
foreach ($orders as $order) {
echo $order->user->name; // 0 consultas extras
echo $order->shop->name; // 0 consultas extras
}
(3) Resultado
Depois que Charlie implementou o pré-carregamento, o número de consultas do painel caiu de 41 para 4, e o tempo de carregamento da página diminuiu de 3 segundos para 300 milissegundos.
3. Relacionamento Um-para-Um
(1) hasOne / belongsTo
// Usuário tem um Profile
class User extends Model
{
public function profile(): HasOne
{
return $this->hasOne(Profile::class);
}
}
// Profile pertence a Usuário
class Profile extends Model
{
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
}
// Uso
$profile = $user->profile;
$user = $profile->user;
| Direção | Método | Posição da Chave Estrangeira | Descrição |
|---|---|---|---|
| User → Profile | hasOne | tabela profiles | tem |
| Profile → User | belongsTo | tabela profiles | pertence a |
(1) ▶ Exemplo: Usuários e Planos de Assinatura do ShopMetrics
// Usuário tem uma assinatura ativa
class User extends Model
{
public function activeSubscription(): HasOne
{
return $this->hasOne(Subscription::class)
->where('status', 'active')
->latestOfMany();
}
}
// Assinatura pertence a Usuário
class Subscription extends Model
{
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
}
// Uso
$plan = $user->activeSubscription->plan;
Saída:
// Execução bem-sucedida
4. Relacionamento Um-para-Muitos
(1) hasMany / belongsTo
// Tenant tem muitas Lojas
class Tenant extends Model
{
public function shops(): HasMany
{
return $this->hasMany(Shop::class);
}
public function orders(): HasMany
{
return $this->hasMany(Order::class);
}
}
// Loja pertence a Tenant
class Shop extends Model
{
public function tenant(): BelongsTo
{
return $this->belongsTo(Tenant::class);
}
public function products(): HasMany
{
return $this->hasMany(Product::class);
}
public function orders(): HasMany
{
return $this->hasMany(Order::class);
}
}
(2) Diagrama UML dos Sete Principais Tipos de Associação
classDiagram
class Tenant {
+shops() HasMany
+orders() HasMany
+users() HasMany
+subscription() HasOne
}
class User {
+tenant() BelongsTo
+profile() HasOne
+orders() HasMany
}
class Shop {
+tenant() BelongsTo
+products() HasMany
+orders() HasMany
}
class Product {
+shop() BelongsTo
+categories() BelongsToMany
}
class Category {
+products() BelongsToMany
}
class Order {
+shop() BelongsTo
+user() BelongsTo
+items() HasMany
}
class OrderItem {
+order() BelongsTo
+product() BelongsTo
}
Tenant "1" --> "*" Shop : hasMany
Tenant "1" --> "*" User : hasMany
Tenant "1" --> "1" Subscription : hasOne
Shop "1" --> "*" Product : hasMany
Shop "1" --> "*" Order : hasMany
Product "*" --> "*" Category : belongsToMany
Order "1" --> "*" OrderItem : hasMany
(1) ▶ Exemplo: Lojas e Pedidos em um Tenant do ShopMetrics
// Obter tenant com todas as lojas e seus pedidos recentes
$tenant = Tenant::with(['shops' => function ($query) {
$query->withCount(['orders' => function ($q) {
$q->where('created_at', '>=', now()->subDays(30));
}])->orderBy('revenue', 'desc');
}])->findOrFail($tenantId);
foreach ($tenant->shops as $shop) {
echo "{$shop->name}: {$shop->orders_count} pedidos recentes";
}
Saída:
// Execução bem-sucedida
5. Relacionamento Muitos-para-Muitos
(1) belongsToMany e Tabelas Pivô
// Produto pertence a muitas Categorias (via pivô category_product)
class Product extends Model
{
public function categories(): BelongsToMany
{
return $this->belongsToMany(Category::class)
->withPivot('is_primary')
->withTimestamps();
}
}
class Category extends Model
{
public function products(): BelongsToMany
{
return $this->belongsToMany(Product::class)
->withPivot('is_primary')
->withTimestamps();
}
}
(2) Estrutura da Tabela Pivô
category_product
├── id
├── category_id (FK)
├── product_id (FK)
├── is_primary (BOOLEAN)
├── created_at
└── updated_at
| Método Pivô | Finalidade |
|---|---|
withPivot() |
Leitura adicional de coluna pivô |
withTimestamps() |
Manter timestamp do pivô |
as('alias') |
Alias para pivô |
wherePivot() |
Filtrar condições do pivô |
sync() |
Sincronizar Associação (Diferença de Conjunto) |
attach() |
Adicionar Associação |
detach() |
Remover Associação |
(1) ▶ Exemplo: Categorias de Produtos do ShopMetrics (Muitos-para-Muitos)
// Vincular categorias a um produto
$product->categories()->attach([1, 2, 3], ['is_primary' => false]);
$product->categories()->attach(4, ['is_primary' => true]);
// Sync — definir categorias exatas (remove outras)
$product->categories()->sync([
1 => ['is_primary' => false],
4 => ['is_primary' => true],
]);
// Sync sem detach — apenas adicionar, não remover
$product->categories()->syncWithoutDetaching([5, 6]);
// Consultar com condição de pivô
$primaryCategory = $product->categories()
->wherePivot('is_primary', true)
->first();
// Desvincular categorias específicas
$product->categories()->detach([1, 2]);
Saída:
// Execução bem-sucedida
6. Associação Remota e Associação Polimórfica
(1) HasManyThrough
// Tenant tem muitos Products através de Shop
class Tenant extends Model
{
public function products(): HasManyThrough
{
return $this->hasManyThrough(
Product::class, // alvo final
Shop::class, // intermediário
'tenant_id', // FK em shops
'shop_id', // FK em products
'id', // PK em tenants
'id', // PK em shops
);
}
}
// Uso: acesso direto sem carregar shops
$products = $tenant->products()->where('is_active', true)->get();
(2) Associações Polimórficas
// Image pode pertencer a Shop ou Product (morphable)
class Image extends Model
{
public function imageable(): MorphTo
{
return $this->morphTo();
}
}
class Shop extends Model
{
public function images(): MorphMany
{
return $this->morphMany(Image::class, 'imageable');
}
}
class Product extends Model
{
public function images(): MorphMany
{
return $this->morphMany(Image::class, 'imageable');
}
}
// Migração para polimórfica
Schema::create('images', function (Blueprint $table) {
$table->id();
$table->morphs('imageable'); // imageable_type + imageable_id
$table->string('path');
$table->timestamps();
});
| Tipo de Associação | Método | Caso de Uso |
|---|---|---|
| Um-para-um | hasOne/belongsTo | User → Profile |
| Um-para-muitos | hasMany/belongsTo | Tenant → Loja |
| Muitos-para-muitos | belongsToMany | Product ↔ Category |
| Um-para-muitos remoto | hasManyThrough | Tenant → Product (via Loja) |
| Polimórfica Um-para-Um | morphOne/morphTo | Image → Product/Loja |
| Polimórfica Um-para-Muitos | morphMany/morphTo | Comments → Products/Artigos |
| Polimórfica Muitos-para-Muitos | morphToMany/morphByMany | Tag → Product/Artigo |
(1) ▶ Exemplo: Sistema de Imagens Polimórficas do ShopMetrics
// Adicionar imagem à loja
$shop->images()->create(['path' => 'shops/alice-store/banner.jpg']);
// Adicionar imagem ao produto
$product->images()->create(['path' => 'products/widget-a/thumb.jpg']);
// Consulta polimórfica — obter dono da imagem
$image = Image::find(1);
$image->imageable; // Retorna instância de Shop ou Product
// Pré-carregar polimórfica
$images = Image::with('imageable')->get();
foreach ($images as $image) {
echo $image->imageable->name; // Funciona tanto para Shop quanto Product
}
Saída:
// Execução bem-sucedida
7. Pré-carregamento e Otimização N+1
(1) O Problema N+1
Sem eager loading:
1. SELECT * FROM orders WHERE tenant_id = 1 LIMIT 10 -- 1 consulta
2. SELECT * FROM users WHERE id = 1 -- +1 por pedido
3. SELECT * FROM users WHERE id = 2
4. SELECT * FROM shops WHERE id = 5
... (até 30+ consultas para 10 pedidos)
(2) Pré-carregamento com with()
// Eager load — 4 consultas total
$orders = Order::with(['user', 'shop', 'items.product'])->paginate(15);
// Pré-carregamento aninhado
$orders = Order::with(['items.product.categories'])->get();
// Pré-carregamento condicional
$orders = Order::with(['items' => function ($query) {
$query->where('quantity', '>', 1);
}])->get();
// Pré-carregamento preguiçoso — carregar após consulta inicial
$orders = Order::all();
if ($needItems) {
$orders->load('items.product');
}
| Método | Momento | Cenários Aplicáveis |
|---|---|---|
with() |
Carregar na consulta | Sabe que precisa de join |
load() |
Carregar após consulta | Carregamento condicional |
loadCount() |
Carregar apenas contagem | Apenas quantidade, sem dados necessários |
loadMissing() |
Carregar quando ausente | Evitar recarregamento |
(1) ▶ Exemplo: Otimização N+1 no Painel do ShopMetrics
// RUIM — problema N+1 no painel
$shops = Shop::where('tenant_id', $tenantId)->get();
foreach ($shops as $shop) {
echo $shop->orders->count(); // +1 consulta por loja
echo $shop->products->count(); // +1 consulta por loja
}
// BOM — withCount + eager loading
$shops = Shop::where('tenant_id', $tenantId)
->withCount(['orders', 'products', 'orders as recent_orders_count' => function ($q) {
$q->where('created_at', '>=', now()->subDays(30));
}])
->with(['latestOrder'])
->get();
foreach ($shops as $shop) {
echo $shop->orders_count; // 0 consultas extras
echo $shop->recent_orders_count; // 0 consultas extras
echo $shop->latestOrder->total; // 0 consultas extras
}
Saída:
// Execução bem-sucedida
8. Exemplo Completo: Agregação de Dados de Tenant do ShopMetrics
// ============================================
// Completo: Agregação de Dados de Tenant do ShopMetrics
// Abrange: todos os tipos de relacionamento, eager loading, pivô, polimórfica
// ============================================
// app/Models/Tenant.php
class Tenant extends Model
{
public function users(): HasMany
{
return $this->hasMany(User::class);
}
public function shops(): HasMany
{
return $this->hasMany(Shop::class);
}
public function orders(): HasMany
{
return $this->hasMany(Order::class);
}
public function subscription(): HasOne
{
return $this->hasOne(Subscription::class)->where('status', 'active');
}
public function products(): HasManyThrough
{
return $this->hasManyThrough(Product::class, Shop::class);
}
public function scopeWithStats(Builder $query): Builder
{
return $query->withCount([
'shops as active_shops_count' => fn ($q) => $q->where('status', 'active'),
'orders as monthly_orders_count' => fn ($q) => $q->whereBetween('created_at', [
now()->startOfMonth(), now()->endOfMonth(),
]),
])->withSum('orders as total_revenue', 'total');
}
}
// Uso — uma consulta com tudo
$tenant = Tenant::withStats()
->with(['subscription.plan', 'shops' => fn ($q) => $q->orderBy('revenue', 'desc')->take(5)])
->findOrFail($tenantId);
$tenant->active_shops_count; // 15
$tenant->monthly_orders_count; // 234
$tenant->total_revenue; // 45678.90
$tenant->subscription->plan->name; // Pro
$tenant->shops->first()->name; // Alice Store
❓ Perguntas Frequentes
hasOne ou hasMany; se a chave estrangeira está nesta tabela, use belongsTo; se nenhuma tabela tem chave estrangeira, use belongsToMany (requer tabela pivô). Um modelo pode pertencer a múltiplos tipos usando relacionamentos polimórficos.with() e load()?with() carrega dados durante a consulta, oferecendo desempenho ideal; load() carrega dados sob demanda após a consulta, sendo adequado para carregamento condicional (como quando dados relacionados são necessários apenas sob certas condições).imageable_type para distinguir entre tipos, então restrições tradicionais de chave estrangeira não podem ser estabelecidas. Adicionar um índice em (imageable_type, imageable_id) pode melhorar o desempenho. Para grandes conjuntos de dados, considere usar tabelas separadas em vez de joins polimórficos.DB::listen() para registrar o número de consultas ou usar laravel/telescope para monitoramento.$shop->products é uma propriedade dinâmica (retorna uma Collection), enquanto $shop->products() é um construtor de consulta de associação (permite consultas encadeadas). O primeiro executa a consulta automaticamente, enquanto o segundo a adia até que get() seja chamado manualmente.📖 Resumo
- Para relacionamentos um-para-um, use
hasOneoubelongsTo; a chave estrangeira está no ladobelongsTo - Relacionamentos um-para-muitos (hasMany/belongsTo) são os tipos de associação mais comuns
- Para relacionamentos muitos-para-muitos, use
belongsToMany; uma tabela pivô é necessária - HasManyThrough: Acessando uma associação remota através de uma tabela intermediária
- Associações polimórficas permitem que um único modelo pertença a múltiplos tipos (ex: image → product/loja)
- Pré-carregamento com with() resolve o problema N+1; withCount() apenas conta o número de registros sem carregar os dados
📝 Exercícios
-
Exercício Básico (⭐): Defina a associação Tenant→Shop→Product para o ShopMetrics. Use Tinker para criar dados de teste e acessar a associação:
$tenant->shops->first()->products. -
Exercício Avançado (⭐⭐): Implemente um relacionamento muitos-para-muitos entre
ProducteCategory, crie uma migração pivô que inclua uma colunais_primary, usesync()para sincronizar categorias, e consulte a categoria principal de um dado produto. -
Desafio (⭐⭐⭐): Implemente um sistema de comentários polimórfico (onde o modelo
Commentpode ser associado tanto a produtos quanto a lojas) e pré-carregue todos os comentários e suas associaçõescommentableno painel, garantindo que apenas 3 instruções SQL sejam necessárias (1 para recuperar comentários + 1 para recuperar produtos + 1 para recuperar lojas).



