404 Not Found

404 Not Found


nginx

Laravel Eloquent Associations

Relationships are Eloquent's "superpower"—with just one line of code, you can bridge the gap between tables using foreign keys and say goodbye to manual JOINs.

1. What You'll Learn


2. A True Story of a Data Analytics Team

(1) Pain Point: 50 queries just to display 10 orders

Charlie sees 10 orders on the ShopMetrics dashboard—he runs one query to retrieve the orders, then for each order, he runs one query for the user, one for the store, and one for the product, totaling 1 + 10 x 4 = 41 SQL queries. Database load skyrocketed, and page load time went from 200 ms to 3 s. Bob added more joins (order → product → category → tag), bringing the number of queries to over 200, and Alice complained that the system was "as slow as a snail."

(2) Preloading Solutions

Eloquent's with() preload retrieves all associated data in a single query, reducing 41 SQL statements to 4.

PHP
// N+1 problem — 41 queries
$orders = Order::take(10)->get();
foreach ($orders as $order) {
    echo $order->user->name;    // +1 query each
    echo $order->shop->name;    // +1 query each
}

// Eager loading — 4 queries total
$orders = Order::with(['user', 'shop', 'items.product'])->take(10)->get();
foreach ($orders as $order) {
    echo $order->user->name;    // 0 extra queries
    echo $order->shop->name;    // 0 extra queries
}

(3) Revenue

After Charlie implemented preloading, the number of dashboard queries dropped from 41 to 4, and page load time decreased from 3 seconds to 300 milliseconds.


3. One-to-One Relationship

(1) hasOne / belongsTo

PHP
// User has one Profile
class User extends Model
{
    public function profile(): HasOne
    {
        return $this->hasOne(Profile::class);
    }
}

// Profile belongs to User
class Profile extends Model
{
    public function user(): BelongsTo
    {
        return $this->belongsTo(User::class);
    }
}

// Usage
$profile = $user->profile;
$user = $profile->user;
Direction Method Foreign Key Position Description
User → Profile hasOne profiles table has
Profile → User belongsTo profiles table belongs to

(1) ▶ Example: ShopMetrics Users and Subscription Plans

PHP
// User has one active subscription
class User extends Model
{
    public function activeSubscription(): HasOne
    {
        return $this->hasOne(Subscription::class)
            ->where('status', 'active')
            ->latestOfMany();
    }
}

// Subscription belongs to User
class Subscription extends Model
{
    public function user(): BelongsTo
    {
        return $this->belongsTo(User::class);
    }
}

// Usage
$plan = $user->activeSubscription->plan;

Output:

TEXT
// Execution Successful

4. One-to-Many Relationship

(1) hasMany / belongsTo

PHP
// Tenant has many Shops
class Tenant extends Model
{
    public function shops(): HasMany
    {
        return $this->hasMany(Shop::class);
    }

    public function orders(): HasMany
    {
        return $this->hasMany(Order::class);
    }
}

// Shop belongs to 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) UML Diagram of the Seven Major Association Relationships

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) ▶ Example: Stores and Orders in a ShopMetrics Tenant

PHP
// Get tenant with all shops and their recent orders
$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} recent orders";
}

Output:

TEXT
// Execution Successful

5. Many-to-Many Relationships

(1) belongsToMany and Pivot Tables

PHP
// Product belongs to many Categories (via category_product pivot)
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) Pivot Table Structure

TEXT
category_product
├── id
├── category_id (FK)
├── product_id  (FK)
├── is_primary  (BOOLEAN)
├── created_at
└── updated_at
Pivot Method Purpose
withPivot() Additional read of the pivot column
withTimestamps() Maintain the pivot's timestamp
as('alias') Alias for pivot
wherePivot() Filter pivot conditions
sync() Synchronization Association (Set Difference)
attach() Add Association
detach() Remove Association

(1) ▶ Example: ShopMetrics Product Categories (Many-to-Many)

PHP
// Attach categories to a product
$product->categories()->attach([1, 2, 3], ['is_primary' => false]);
$product->categories()->attach(4, ['is_primary' => true]);

// Sync — set exact categories (removes others)
$product->categories()->sync([
    1 => ['is_primary' => false],
    4 => ['is_primary' => true],
]);

// Sync without detaching — add only, don't remove
$product->categories()->syncWithoutDetaching([5, 6]);

// Query with pivot condition
$primaryCategory = $product->categories()
    ->wherePivot('is_primary', true)
    ->first();

// Detach specific categories
$product->categories()->detach([1, 2]);

Output:

TEXT
// Execution Successful

6. Distant Correlation and Polymorphic Correlation

(1) HasManyThrough

PHP
// Tenant has many Products through Shop
class Tenant extends Model
{
    public function products(): HasManyThrough
    {
        return $this->hasManyThrough(
            Product::class,    // final target
            Shop::class,       // intermediate
            'tenant_id',       // FK on shops
            'shop_id',         // FK on products
            'id',              // PK on tenants
            'id',              // PK on shops
        );
    }
}

// Usage: direct access without loading shops
$products = $tenant->products()->where('is_active', true)->get();

(2) Polymorphic Associations

PHP
// Image can belong to Shop or 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');
    }
}

// Migration for polymorphic
Schema::create('images', function (Blueprint $table) {
    $table->id();
    $table->morphs('imageable'); // imageable_type + imageable_id
    $table->string('path');
    $table->timestamps();
});
Association Type Method Use Case
One-to-one hasOne/belongsTo User → Profile
One-to-many hasMany/belongsTo Tenant → Store
Many-to-Many belongsToMany Product ↔ Category
Remote One-to-Many hasManyThrough Tenant → Product (via Store)
Polymorphic One-to-One morphOne/morphTo Image → Product/Store
Polymorphic One-to-Many morphMany/morphTo Comments → Products/Articles
Polymorphic Many-to-Many morphToMany/morphByMany Tag → Product/Article

(1) ▶ Example: ShopMetrics Polymorphic Image System

PHP
// Add image to shop
$shop->images()->create(['path' => 'shops/alice-store/banner.jpg']);

// Add image to product
$product->images()->create(['path' => 'products/widget-a/thumb.jpg']);

// Query polymorphic — get image's owner
$image = Image::find(1);
$image->imageable; // Returns Shop or Product instance

// Eager load polymorphic
$images = Image::with('imageable')->get();
foreach ($images as $image) {
    echo $image->imageable->name; // Works for both Shop and Product
}

Output:

TEXT
// Execution Successful

7. Preloading and N+1 Optimization

(1) The N+1 Problem

TEXT
Without eager loading:
1. SELECT * FROM orders WHERE tenant_id = 1 LIMIT 10     -- 1 query
2. SELECT * FROM users WHERE id = 1                       -- +1 per order
3. SELECT * FROM users WHERE id = 2
4. SELECT * FROM shops WHERE id = 5
... (up to 30+ queries for 10 orders)

(2) with() Preloading

PHP
// Eager load — 4 queries total
$orders = Order::with(['user', 'shop', 'items.product'])->paginate(15);

// Nested eager loading
$orders = Order::with(['items.product.categories'])->get();

// Conditional eager loading
$orders = Order::with(['items' => function ($query) {
    $query->where('quantity', '>', 1);
}])->get();

// Lazy eager loading — load after initial query
$orders = Order::all();
if ($needItems) {
    $orders->load('items.product');
}
Method Timing Applicable Scenarios
with() Load on query Known to require a join
load() Load after query Conditional loading
loadCount() Load count only Quantity only, no data required
loadMissing() Load when missing Avoid reloading

(1) ▶ Example: ShopMetrics Dashboard N+1 Optimization

PHP
// BAD — N+1 problem in dashboard
$shops = Shop::where('tenant_id', $tenantId)->get();
foreach ($shops as $shop) {
    echo $shop->orders->count();       // +1 query per shop
    echo $shop->products->count();     // +1 query per shop
}

// GOOD — 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 extra queries
    echo $shop->recent_orders_count;    // 0 extra queries
    echo $shop->latestOrder->total;     // 0 extra queries
}

Output:

TEXT
// Execution Successful

8. Comprehensive Example: ShopMetrics Tenant Data Aggregation

PHP
// ============================================
// Comprehensive: ShopMetrics Tenant Data Aggregation
// Covers: all relationship types, eager loading, pivot, polymorphic
// ============================================

// 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');
    }
}

// Usage — one query with everything
$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

❓ FAQ

Q How do I determine which relationship to use?
A Look at the location of the foreign key—if the foreign key is in the other table, use hasOne or hasMany; if the foreign key is in this table, use belongsTo; if neither table has a foreign key, use belongsToMany (requires a pivot table). A model can belong to multiple types using polymorphic relationships.
Q When should I use with() and load()?
A with() loads data during the query, offering optimal performance; load() loads data on demand after the query, making it suitable for conditional loading (such as when related data is needed only under certain conditions).
Q What is the difference between "sync" and "attach"?
A "Attach" simply adds a relationship (without removing the old one), while "sync" sets up an exact list of relationships (any excess entries are "detached"). Use "sync" for "full replacement" and "attach" for "incremental addition."
Q Does a polymorphic join affect query performance?
A Polymorphic joins use the imageable_type column to distinguish between types, so traditional foreign key constraints cannot be established. Adding an index on (imageable_type, imageable_id) can improve performance. For large datasets, consider using separate tables instead of polymorphic joins.
Q How do I detect the N+1 problem?
A Install Laravel Debugbar and check the number of SQL queries on each page. If there are more than 20, it usually indicates an N+1 problem. You can also use DB::listen() to log the number of queries or use laravel/telescope for monitoring.
Q What is the difference between using an association method with and without parentheses?
A $shop->products is a dynamic property (returns a Collection), while $shop->products() is an association query constructor (allows for chained queries). The former executes the query automatically, while the latter defers it until get() is manually called.

📖 Summary


📝 Exercises

  1. Basic Exercise (⭐): Define the Tenant→Shop→Product association for ShopMetrics. Use Tinker to create test data and access the association: $tenant->shops->first()->products.

  2. Advanced Exercise (⭐⭐): Implement a many-to-many relationship between Product and Category, create a pivot migration that includes an is_primary column, use sync() to synchronize categories, and query the primary category for a given product.

  3. Challenge (⭐⭐⭐): Implement a polymorphic comment system (where the Comment model can be associated with both products and stores) and preload all comments and their commentable associations in the dashboard, ensuring that only 3 SQL statements are required (1 to retrieve comments + 1 to retrieve products + 1 to retrieve stores).

Web-Tutorial.com

Web-Tutorial Tech Team

A team of developers maintaining programming tutorials. Each tutorial is written and reviewed by developers with expertise in that field. We work to keep our content accurate and reliable — if you spot an issue, please let us know.

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏