404 Not Found

404 Not Found


nginx

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


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.

PHP
// 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

PHP
// 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

PHP
// 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:

TEXT
// Execução bem-sucedida

4. Relacionamento Um-para-Muitos

(1) hasMany / belongsTo

PHP
// 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

100%
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

PHP
// 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:

TEXT
// Execução bem-sucedida

5. Relacionamento Muitos-para-Muitos

(1) belongsToMany e Tabelas Pivô

PHP
// 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ô

TEXT
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)

PHP
// 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:

TEXT
// Execução bem-sucedida

6. Associação Remota e Associação Polimórfica

(1) HasManyThrough

PHP
// 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

PHP
// 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

PHP
// 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:

TEXT
// Execução bem-sucedida

7. Pré-carregamento e Otimização N+1

(1) O Problema N+1

TEXT
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()

PHP
// 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

PHP
// 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:

TEXT
// Execução bem-sucedida

8. Exemplo Completo: Agregação de Dados de Tenant do ShopMetrics

PHP
// ============================================
// 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

P Como determinar qual relacionamento usar?
R Observe a posição da chave estrangeira—se a chave estrangeira está na outra tabela, use 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.
P Quando devo usar with() e load()?
R 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).
P Qual é a diferença entre "sync" e "attach"?
R "Attach" simplesmente adiciona um relacionamento (sem remover o antigo), enquanto "sync" define uma lista exata de relacionamentos (quaisquer entradas excedentes são "detached"). Use "sync" para "substituição completa" e "attach" para "adição incremental".
P Um join polimórfico afeta o desempenho da consulta?
R Joins polimórficos usam a coluna 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.
P Como detecto o problema N+1?
R Instale o Laravel Debugbar e verifique o número de consultas SQL em cada página. Se houver mais de 20, geralmente indica um problema N+1. Você também pode usar DB::listen() para registrar o número de consultas ou usar laravel/telescope para monitoramento.
P Qual é a diferença entre usar um método de associação com e sem parênteses?
R $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


📝 Exercícios

  1. 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.

  2. Exercício Avançado (⭐⭐): Implemente um relacionamento muitos-para-muitos entre Product e Category, crie uma migração pivô que inclua uma coluna is_primary, use sync() para sincronizar categorias, e consulte a categoria principal de um dado produto.

  3. Desafio (⭐⭐⭐): Implemente um sistema de comentários polimórfico (onde o modelo Comment pode ser associado tanto a produtos quanto a lojas) e pré-carregue todos os comentários e suas associações commentable no painel, garantindo que apenas 3 instruções SQL sejam necessárias (1 para recuperar comentários + 1 para recuperar produtos + 1 para recuperar lojas).

Web-Tutorial.com

Equipe Técnica Web-Tutorial

Uma plataforma de tutoriais mantida por diversos desenvolvedores. Cada tutorial é escrito e revisado por profissionais da área correspondente. Trabalhamos para manter nosso conteúdo preciso e confiável — se encontrar algum problema, avise-nos.

100%