Skip to content

Eloquent:關聯

簡介

資料庫表通常彼此相關。例如,一篇部落格文章可能有許多留言,或者一筆訂單可能與下單的使用者相關。Eloquent 讓管理與處理這些關聯變得簡單,並且支援各種常見的關聯:

定義關聯

Eloquent 關聯是在您的 Eloquent 模型類別中以方法的方式定義的。由於關聯也具備強大的 查詢產生器 (query builders) 功能,將關聯定義為方法提供了強大的方法鏈結與查詢能力。例如,我們可以在這個 posts 關聯上鏈結額外的查詢約束:

php
$user->posts()->where('active', 1)->get();

但在深入了解如何使用關聯之前,讓我們學習如何定義 Eloquent 支援的每種關聯類型。

一對一 / Has One

一對一關聯是非常基本的資料庫關聯類型。例如,一個 User 模型可能關聯到一個 Phone 模型。要定義這種關聯,我們會在 User 模型上放置一個 phone 方法。這個 phone 方法應該呼叫 hasOne 方法並回傳其結果。透過模型的 Illuminate\Database\Eloquent\Model 基底類別,您的模型可以使用 hasOne 方法:

php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasOne;

class User extends Model
{
    /**
     * Get the phone associated with the user.
     */
    public function phone(): HasOne
    {
        return $this->hasOne(Phone::class);
    }
}

傳遞給 hasOne 方法的第一個參數是關聯模型的類別名稱。一旦定義了關聯,我們就可以使用 Eloquent 的動態屬性來取出相關紀錄。動態屬性允許您像訪問模型上定義的屬性一樣訪問關聯方法:

php
$phone = User::find(1)->phone;

Eloquent 根據父模型名稱來決定關聯的外鍵。在這種情況下,自動假設 Phone 模型有一個 user_id 外鍵。如果您希望覆蓋此慣例,可以將第二個參數傳遞給 hasOne 方法:

php
return $this->hasOne(Phone::class, 'foreign_key');

此外,Eloquent 假設外鍵的值應該與父模型的主鍵欄位匹配。換句話說,Eloquent 會在 Phone 紀錄的 user_id 欄位中尋找使用者的 id 欄位值。如果您希望關聯使用 id 或模型的 $primaryKey 屬性以外的主鍵值,可以將第三個參數傳遞給 hasOne 方法:

php
return $this->hasOne(Phone::class, 'foreign_key', 'local_key');

定義關聯的反向

所以,我們可以從 User 模型訪問 Phone 模型。接下來,讓我們在 Phone 模型上定義一個關聯,讓我們能夠訪問擁有該電話的使用者。我們可以使用 belongsTo 方法定義 hasOne 關聯的反向:

php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;

class Phone extends Model
{
    /**
     * Get the user that owns the phone.
     */
    public function user(): BelongsTo
    {
        return $this->belongsTo(User::class);
    }
}

呼叫 user 方法時,Eloquent 會嘗試尋找一個 User 模型,其 idPhone 模型上的 user_id 欄位匹配。

Eloquent 透過檢查關聯方法的名稱並在方法名稱後加上 _id 綴詞來決定外鍵名稱。因此,在這種情況下,Eloquent 假設 Phone 模型有一個 user_id 欄位。但是,如果 Phone 模型上的外鍵不是 user_id,您可以將自定義鍵名作為第二個參數傳遞給 belongsTo 方法:

php
/**
 * Get the user that owns the phone.
 */
public function user(): BelongsTo
{
    return $this->belongsTo(User::class, 'foreign_key');
}

如果父模型不使用 id 作為主鍵,或者您希望使用不同的欄位來尋找相關聯的模型,您可以將第三個參數傳遞給 belongsTo 方法,指定父資料表的自定義鍵:

php
/**
 * Get the user that owns the phone.
 */
public function user(): BelongsTo
{
    return $this->belongsTo(User::class, 'foreign_key', 'owner_key');
}

一對多 / Has Many

一對多關聯用於定義單個模型是多個子模型父級的關聯。例如,一篇部落格文章可能有無限數量的留言。與所有其他 Eloquent 關聯一樣,一對多關聯是透過在您的 Eloquent 模型中定義一個方法來定義的:

php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;

class Post extends Model
{
    /**
     * Get the comments for the blog post.
     */
    public function comments(): HasMany
    {
        return $this->hasMany(Comment::class);
    }
}

請記住,Eloquent 將自動為 Comment 模型決定適當的外鍵欄位。按照慣例,Eloquent 將採用父模型的「蛇形命名 (snake case)」名稱並加上 _id 字尾。因此,在此範例中,Eloquent 會假設 Comment 模型上的外鍵欄位是 post_id

一旦定義了關聯方法,我們就可以透過訪問 comments 屬性來取出相關留言的 集合 (collection)。請記住,由於 Eloquent 提供「動態關聯屬性」,我們可以像訪問模型上定義的屬性一樣訪問關聯方法:

php
use App\Models\Post;

$comments = Post::find(1)->comments;

foreach ($comments as $comment) {
    // ...
}

由於所有關聯也都作為查詢產生器,您可以透過呼叫 comments 方法並繼續鏈結查詢條件,向關聯查詢添加進一步的約束:

php
$comment = Post::find(1)->comments()
    ->where('title', 'foo')
    ->first();

hasOne 方法一樣,您也可以透過向 hasMany 方法傳遞額外參數來覆蓋外鍵與本地鍵:

php
return $this->hasMany(Comment::class, 'foreign_key');

return $this->hasMany(Comment::class, 'foreign_key', 'local_key');

自動在子模型上填充父模型

即使使用了 Eloquent 積極載入,如果您在遍歷子模型時嘗試從子模型訪問父模型,也可能會出現「N + 1」查詢問題:

php
$posts = Post::with('comments')->get();

foreach ($posts as $post) {
    foreach ($post->comments as $comment) {
        echo $comment->post->title;
    }
}

在上面的範例中,引入了「N + 1」查詢問題,因為即使為每個 Post 模型積極載入了留言,Eloquent 也不會自動為每個子 Comment 模型填充父 Post 模型。

如果您希望 Eloquent 自動將父模型填充到其子模型上,您可以在定義 hasMany 關聯時呼叫 chaperone 方法:

php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;

class Post extends Model
{
    /**
     * Get the comments for the blog post.
     */
    public function comments(): HasMany
    {
        return $this->hasMany(Comment::class)->chaperone();
    }
}

或者,如果您希望在執行時選擇加入自動父模型填充,可以在積極載入關聯時呼叫 chaperone 方法:

php
use App\Models\Post;

$posts = Post::with([
    'comments' => fn ($comments) => $comments->chaperone(),
])->get();

一對多 (反向) / Belongs To

既然我們已經可以存取貼文的所有留言了,接著讓我們定義一個關聯,讓留言可以存取其所屬的父級貼文。若要定義 hasMany 關聯的反向,請在子模型上定義一個呼叫 belongsTo 方法的關聯方法:

php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;

class Comment extends Model
{
    /**
     * Get the post that owns the comment.
     */
    public function post(): BelongsTo
    {
        return $this->belongsTo(Post::class);
    }
}

關聯定義完成後,我們就可以透過存取 post「動態關聯屬性」來取得留言的父級貼文:

php
use App\Models\Comment;

$comment = Comment::find(1);

return $comment->post->title;

在上述範例中,Eloquent 會嘗試尋找一個 Post 模型,其 idComment 模型上的 post_id 欄位匹配。

Eloquent 會透過檢查關聯方法的名稱,並在該名稱後方加上 _ 以及父模型主鍵欄位的名稱來決定預設的外鍵名稱。因此,在這個範例中,Eloquent 會假設 Post 模型在 comments 資料表上的外鍵是 post_id

然而,如果您的關聯外鍵不符合這些慣例,您可以將自定義的外鍵名稱作為第二個參數傳遞給 belongsTo 方法:

php
/**
 * Get the post that owns the comment.
 */
public function post(): BelongsTo
{
    return $this->belongsTo(Post::class, 'foreign_key');
}

如果您的父模型不使用 id 作為主鍵,或者您希望使用不同的欄位來尋找關聯模型,您可以向 belongsTo 方法傳遞第三個參數,指定父資料表的自定義鍵名:

php
/**
 * Get the post that owns the comment.
 */
public function post(): BelongsTo
{
    return $this->belongsTo(Post::class, 'foreign_key', 'owner_key');
}

預設模型

belongsTohasOnehasOneThrough 以及 morphOne 關聯允許您定義一個預設模型,當給定的關聯為 null 時將會回傳該模型。這種模式通常被稱為 Null Object 模式,有助於移除程式碼中的條件檢查。在以下範例中,如果 Post 模型沒有關聯任何使用者,則 user 關聯將回傳一個空的 App\Models\User 模型:

php
/**
 * Get the author of the post.
 */
public function user(): BelongsTo
{
    return $this->belongsTo(User::class)->withDefault();
}

若要使用屬性填充預設模型,您可以向 withDefault 方法傳遞一個陣列或閉包:

php
/**
 * Get the author of the post.
 */
public function user(): BelongsTo
{
    return $this->belongsTo(User::class)->withDefault([
        'name' => 'Guest Author',
    ]);
}

/**
 * Get the author of the post.
 */
public function user(): BelongsTo
{
    return $this->belongsTo(User::class)->withDefault(function (User $user, Post $post) {
        $user->name = 'Guest Author';
    });
}

查詢 Belongs To 關聯

當查詢「屬於 (belongs to)」關聯的子模型時,您可以手動建立 where 子句來取得對應的 Eloquent 模型:

php
use App\Models\Post;

$posts = Post::where('user_id', $user->id)->get();

不過,您可能會發現使用 whereBelongsTo 方法更方便,它會自動為給定的模型判斷正確的關聯與外鍵:

php
$posts = Post::whereBelongsTo($user)->get();

您也可以將 Collection 實例提供給 whereBelongsTo 方法。這樣做時,Laravel 將會取得屬於該 Collection 中任何父模型的所有子模型:

php
$users = User::where('vip', true)->get();

$posts = Post::whereBelongsTo($users)->get();

預設情況下,Laravel 會根據模型的類別名稱來判斷與給定模型相關聯的關聯;不過,您可以透過向 whereBelongsTo 方法提供第二個參數來手動指定關聯名稱:

php
$posts = Post::whereBelongsTo($user, 'author')->get();

多個中的一個 (Has One of Many)

有時一個模型可能有多個相關模型,但您希望輕鬆取出該關聯中「最新」或「最舊」的相關模型。例如,一個 User 模型可能與許多 Order 模型相關聯,但您想定義一個方便的方法來與該使用者最近下的一筆訂單進行互動。您可以使用 hasOne 關聯類型結合 ofMany 方法來實現這一點:

php
/**
 * Get the user's most recent order.
 */
public function latestOrder(): HasOne
{
    return $this->hasOne(Order::class)->latestOfMany();
}

同樣地,您可以定義一個方法來取出關聯中「最舊」或第一個相關的模型:

php
/**
 * Get the user's oldest order.
 */
public function oldestOrder(): HasOne
{
    return $this->hasOne(Order::class)->oldestOfMany();
}

預設情況下,latestOfManyoldestOfMany 方法會根據模型的初級鍵 (Primary Key) 來取出最新或最舊的相關模型,該鍵必須是可排序的。然而,有時您可能希望使用不同的排序標準從較大的關聯中取出單個模型。

例如,使用 ofMany 方法,您可以取出使用者最昂貴的訂單。ofMany 方法接受可排序的欄位作為其第一個參數,以及在查詢相關模型時要套用的聚合函式 (minmax):

php
/**
 * Get the user's largest order.
 */
public function largestOrder(): HasOne
{
    return $this->hasOne(Order::class)->ofMany('price', 'max');
}

⚠️ 警告

由於 PostgreSQL 不支援對 UUID 欄位執行 MAX 函式,目前無法將「多個中的一個」關聯與 PostgreSQL UUID 欄位結合使用。

將「多」個關聯轉換為 Has One 關聯

通常,在使用 latestOfManyoldestOfManyofMany 方法取出單個模型時,您已經為同一個模型定義了「has many」關聯。為了方便起見,Laravel 允許您透過在關聯上呼叫 one 方法,輕鬆地將此關聯轉換為「has one」關聯:

php
/**
 * Get the user's orders.
 */
public function orders(): HasMany
{
    return $this->hasMany(Order::class);
}

/**
 * Get the user's largest order.
 */
public function largestOrder(): HasOne
{
    return $this->orders()->one()->ofMany('price', 'max');
}

您還可以使用 one 方法將 HasManyThrough 關聯轉換為 HasOneThrough 關聯:

php
public function latestDeployment(): HasOneThrough
{
    return $this->deployments()->one()->latestOfMany();
}

進階的多個中的一個關聯

建構更進階的「多個中的一個」關聯是可行的。例如,一個 Product 模型可能有多個關聯的 Price 模型,即使在發布新價格後,這些模型仍保留在系統中。此外,產品的新價格數據可能可以提前發布,透過 published_at 欄位在未來的日期生效。

總結來說,我們需要取出發布日期不在未來且最新發布的價格。此外,如果兩個價格具有相同的發布日期,我們將優先選擇 ID 較大的價格。為了實現這一點,我們必須向 ofMany 方法傳遞一個陣列,其中包含決定最新價格的可排序欄位。此外,將提供一個閉包 (Closure) 作為 ofMany 方法的第二個參數。此閉包將負責向關聯查詢添加額外的發布日期限制:

php
/**
 * Get the current pricing for the product.
 */
public function currentPricing(): HasOne
{
    return $this->hasOne(Price::class)->ofMany([
        'published_at' => 'max',
        'id' => 'max',
    ], function (Builder $query) {
        $query->where('published_at', '<', now());
    });
}

遠處一對一 (Has One Through)

「遠處一對一 (has-one-through)」關聯定義了與另一個模型的一對一關聯。然而,此關聯表示宣告模型可以透過「經過 (through)」第三個模型來與另一個模型的單個實例匹配。

例如,在一個車輛維修場應用程式中,每個 Mechanic 模型可能與一個 Car 模型相關聯,而每個 Car 模型可能與一個 Owner 模型相關聯。雖然技師和車主在資料庫中沒有直接關係,但技師可以「透過」 Car 模型存取車主。讓我們看看定義此關聯所需的資料表:

text
mechanics
    id - integer
    name - string

cars
    id - integer
    model - string
    mechanic_id - integer

owners
    id - integer
    name - string
    car_id - integer

既然我們已經檢查了關聯的資料表結構,讓我們在 Mechanic 模型上定義該關聯:

php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasOneThrough;

class Mechanic extends Model
{
    /**
     * Get the car's owner.
     */
    public function carOwner(): HasOneThrough
    {
        return $this->hasOneThrough(Owner::class, Car::class);
    }
}

傳遞給 hasOneThrough 方法的第一個參數是我們希望存取的最終模型名稱,而第二個參數是中間模型的名稱。

或者,如果相關關聯已經在關聯中涉及的所有模型上定義,您可以透過呼叫 through 方法並提供這些關聯的名稱來流暢地定義「遠處一對一」關聯。例如,如果 Mechanic 模型具有 cars 關聯,而 Car 模型具有 owner 關聯,您可以定義一個連接技師和車主的「遠處一對一」關聯,如下所示:

php
// String based syntax...
return $this->through('cars')->has('owner');

// Dynamic syntax...
return $this->throughCars()->hasOwner();

鍵值慣例

執行關聯查詢時,將使用典型的 Eloquent 外鍵 (Foreign Key) 慣例。如果您想自定義關聯的鍵,可以將它們作為第三和第四個參數傳遞給 hasOneThrough 方法。第三個參數是中間模型上的外鍵名稱。第四個參數是最終模型上的外鍵名稱。第五個參數是本地鍵 (Local Key),而第六個參數是中間模型的本地鍵:

php
class Mechanic extends Model
{
    /**
     * Get the car's owner.
     */
    public function carOwner(): HasOneThrough
    {
        return $this->hasOneThrough(
            Owner::class,
            Car::class,
            'mechanic_id', // Foreign key on the cars table...
            'car_id', // Foreign key on the owners table...
            'id', // Local key on the mechanics table...
            'id' // Local key on the cars table...
        );
    }
}

或者,如前所述,如果相關關聯已經在關聯中涉及的所有模型上定義,您可以透過呼叫 through 方法並提供這些關聯的名稱來流暢地定義「遠處一對一」關聯。這種方法的優點是重用了現有關聯上已經定義的鍵值慣例:

php
// String based syntax...
return $this->through('cars')->has('owner');

// Dynamic syntax...
return $this->throughCars()->hasOwner();

遠處一對多 (Has Many Through)

「遠處一對多 (has-many-through)」關聯提供了一種方便的方法,可以透過中間關聯來存取遠處的關聯。例如,假設我們正在建立一個像 Laravel Cloud 這樣的部署平台。一個 Application 模型可能會透過中間的 Environment 模型存取許多 Deployment 模型。利用這個範例,您可以輕鬆地收集特定應用程式的所有部署。讓我們看看定義此關聯所需的資料表:

text
applications
    id - integer
    name - string

environments
    id - integer
    application_id - integer
    name - string

deployments
    id - integer
    environment_id - integer
    commit_hash - string

現在我們已經檢查了資料表結構,讓我們在 Application 模型上定義這個關聯:

php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasManyThrough;

class Application extends Model
{
    /**
     * Get all of the deployments for the application.
     */
    public function deployments(): HasManyThrough
    {
        return $this->hasManyThrough(Deployment::class, Environment::class);
    }
}

傳遞給 hasManyThrough 方法的第一個參數是我們希望存取的最終模型名稱,而第二個參數是中間模型的名稱。

或者,如果所有涉及的模型都已經定義了相關的關聯,您可以使用 through 方法並提供這些關聯的名稱,來流暢地定義一個「遠處一對多」關聯。例如,如果 Application 模型具有 environments 關聯,而 Environment 模型具有 deployments 關聯,您可以像這樣定義連接應用程式與部署的「遠處一對多」關聯:

php
// String based syntax...
return $this->through('environments')->has('deployments');

// Dynamic syntax...
return $this->throughEnvironments()->hasDeployments();

雖然 Deployment 模型的資料表不包含 application_id 欄位,但 hasManyThrough 關聯提供了透過 $application->deployments 存取應用程式部署的方法。為了取得這些模型,Eloquent 會檢查中間 Environment 模型資料表中的 application_id 欄位。在找到相關的環境 ID 後,它們將被用於查詢 Deployment 模型的資料表。

鍵值慣例 (Key Conventions)

執行關聯查詢時,將使用典型的 Eloquent 外鍵慣例。如果您想自定義關聯的鍵值,可以將它們作為第三個和第四個參數傳遞給 hasManyThrough 方法。第三個參數是中間模型上的外鍵名稱。第四個參數是最終模型上的外鍵名稱。第五個參數是本地鍵,而第六個參數是中間模型的本地鍵:

php
class Application extends Model
{
    public function deployments(): HasManyThrough
    {
        return $this->hasManyThrough(
            Deployment::class,
            Environment::class,
            'application_id', // Foreign key on the environments table...
            'environment_id', // Foreign key on the deployments table...
            'id', // Local key on the applications table...
            'id' // Local key on the environments table...
        );
    }
}

或者,如前所述,如果所有涉及的模型都已經定義了相關關聯,您可以透過調用 through 方法並提供這些關聯的名稱來流暢地定義一個「遠處一對多」關聯。這種方法的優點在於可以重複使用現有關聯中已經定義的鍵值慣例:

php
// String based syntax...
return $this->through('environments')->has('deployments');

// Dynamic syntax...
return $this->throughEnvironments()->hasDeployments();

限定範圍關聯 (Scoped Relationships)

通常會在模型中增加額外的方法來約束關聯。例如,您可能會在 User 模型中增加一個 featuredPosts 方法,該方法會透過額外的 where 約束來限制更廣泛的 posts 關聯:

php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;

class User extends Model
{
    /**
     * Get the user's posts.
     */
    public function posts(): HasMany
    {
        return $this->hasMany(Post::class)->latest();
    }

    /**
     * Get the user's featured posts.
     */
    public function featuredPosts(): HasMany
    {
        return $this->posts()->where('featured', true);
    }
}

但是,如果您嘗試透過 featuredPosts 方法建立模型,其 featured 屬性不會被設置為 true。如果您想透過關聯方法建立模型,並同時指定應添加到透過該關聯建立的所有模型中的屬性,您可以在建構關聯查詢時使用 withAttributes 方法:

php
/**
 * Get the user's featured posts.
 */
public function featuredPosts(): HasMany
{
    return $this->posts()->withAttributes(['featured' => true]);
}

withAttributes 方法將使用給定的屬性在查詢中增加 where 條件,它還會將給定的屬性增加到透過該關聯方法建立的任何模型中:

php
$post = $user->featuredPosts()->create(['title' => 'Featured Post']);

$post->featured; // true

要指示 withAttributes 方法不要在查詢中增加 where 條件,您可以將 asConditions 參數設置為 false

php
return $this->posts()->withAttributes(['featured' => true], asConditions: false);

多對多關聯

多對多關聯比 hasOnehasMany 關聯稍微複雜一點。多對多關聯的一個例子是一個 User (使用者) 擁有多個 Role (角色),且這些角色也同時被應用程式中的其他使用者共享。例如,一個使用者可能被分配了「作者」和「編輯」角色;然而,這些角色也可能被分配給其他使用者。因此,一個使用者有多個角色,而一個角色也有多個使用者。

資料表結構

要定義這種關聯,需要三個資料表:usersrolesrole_userrole_user 資料表的名稱是根據相關模型名稱的字母順序衍生的,並包含 user_idrole_id 欄位。此資料表用作連結使用者和角色的中間表。

請記住,由於一個角色可以屬於多個使用者,我們不能簡單地在 roles 資料表中放置一個 user_id 欄位。這意味著一個角色只能屬於單個使用者。為了支援將角色分配給多個使用者,需要 role_user 資料表。我們可以將該關聯的資料表結構總結如下:

text
users
    id - integer
    name - string

roles
    id - integer
    name - string

role_user
    user_id - integer
    role_id - integer

模型結構

多對多關聯是透過編寫一個回傳 belongsToMany 方法結果的方法來定義的。belongsToMany 方法由 Illuminate\Database\Eloquent\Model 基底類別提供,該類別被應用程式中所有的 Eloquent 模型所使用。例如,讓我們在 User 模型上定義一個 roles 方法。傳遞給此方法的第一個參數是相關模型類別的名稱:

php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;

class User extends Model
{
    /**
     * The roles that belong to the user.
     */
    public function roles(): BelongsToMany
    {
        return $this->belongsToMany(Role::class);
    }
}

一旦定義了關聯,您就可以使用 roles 動態關聯屬性來存取使用者的角色:

php
use App\Models\User;

$user = User::find(1);

foreach ($user->roles as $role) {
    // ...
}

由於所有關聯也都充當查詢產生器,您可以透過呼叫 roles 方法並繼續在查詢上鏈結條件來為關聯查詢增加進一步的約束:

php
$roles = User::find(1)->roles()->orderBy('name')->get();

為了確定關聯中間表的名稱,Eloquent 將按字母順序連接兩個相關模型的名稱。但是,您可以自由地覆蓋此慣例。您可以透過向 belongsToMany 方法傳遞第二個參數來做到這一點:

php
return $this->belongsToMany(Role::class, 'role_user');

除了自定義中間表的名稱外,您還可以透過向 belongsToMany 方法傳遞額外參數來自定義資料表上鍵的欄位名稱。第三個參數是您正在定義關聯的模型的外鍵名稱,而第四個參數是您要連接的模型的外鍵名稱:

php
return $this->belongsToMany(Role::class, 'role_user', 'user_id', 'role_id');

定義反向關聯

要定義多對多關聯的「反向」關聯,您應該在相關模型上定義一個同樣回傳 belongsToMany 方法結果的方法。為了完成我們的使用者 / 角色範例,讓我們在 Role 模型上定義 users 方法:

php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;

class Role extends Model
{
    /**
     * The users that belong to the role.
     */
    public function users(): BelongsToMany
    {
        return $this->belongsToMany(User::class);
    }
}

如您所見,除了引用 App\Models\User 模型外,該關聯的定義與其 User 模型對應項完全相同。由於我們重複使用了 belongsToMany 方法,因此在定義多對多關聯的「反向」關聯時,所有常用的資料表和鍵自定義選項都是可用的。

取出中間表欄位

正如您已經學過的,處理多對多關聯需要中間表的存在。Eloquent 提供了一些非常有用的方式來與此表進行互動。例如,讓我們假設我們的 User 模型與許多 Role 模型相關聯。在存取此關聯後,我們可以使用模型上的 pivot 屬性來存取中間表:

php
use App\Models\User;

$user = User::find(1);

foreach ($user->roles as $role) {
    echo $role->pivot->created_at;
}

請注意,我們取出的每個 Role 模型都會自動被分配一個 pivot 屬性。此屬性包含一個代表中間表的模型。

預設情況下,pivot 模型上只會存在模型鍵。如果您的中間表包含額外的屬性,您必須在定義關聯時指定它們:

php
return $this->belongsToMany(Role::class)->withPivot('active', 'created_by');

如果您希望中間表具有由 Eloquent 自動維護的 created_atupdated_at 時間戳記,請在定義關聯時呼叫 withTimestamps 方法:

php
return $this->belongsToMany(Role::class)->withTimestamps();

⚠️ 警告

使用 Eloquent 自動維護時間戳記的中間表必須同時具備 created_atupdated_at 時間戳記欄位。

自定義 pivot 屬性名稱

如前所述,中間表的屬性可以透過 pivot 屬性在模型上存取。但是,您可以自由地自定義此屬性的名稱,以更好地反映其在應用程式中的用途。

例如,如果您的應用程式包含可以訂閱 Podcast 的使用者,那麼使用者和 Podcast 之間可能存在多對多關聯。如果是這種情況,您可能希望將中間表屬性重命名為 subscription 而不是 pivot。這可以在定義關聯時使用 as 方法來完成:

php
return $this->belongsToMany(Podcast::class)
    ->as('subscription')
    ->withTimestamps();

一旦指定了自定義的中間表屬性,您就可以使用自定義名稱來存取中間表資料:

php
$users = User::with('podcasts')->get();

foreach ($users->flatMap->podcasts as $podcast) {
    echo $podcast->subscription->created_at;
}

透過中間表欄位過濾查詢

您也可以在定義關聯時,使用 wherePivotwherePivotInwherePivotNotInwherePivotBetweenwherePivotNotBetweenwherePivotNull 以及 wherePivotNotNull 方法來過濾 belongsToMany 關聯查詢所回傳的結果:

php
return $this->belongsToMany(Role::class)
    ->wherePivot('approved', 1);

return $this->belongsToMany(Role::class)
    ->wherePivotIn('priority', [1, 2]);

return $this->belongsToMany(Role::class)
    ->wherePivotNotIn('priority', [1, 2]);

return $this->belongsToMany(Podcast::class)
    ->as('subscriptions')
    ->wherePivotBetween('created_at', ['2020-01-01 00:00:00', '2020-12-31 00:00:00']);

return $this->belongsToMany(Podcast::class)
    ->as('subscriptions')
    ->wherePivotNotBetween('created_at', ['2020-01-01 00:00:00', '2020-12-31 00:00:00']);

return $this->belongsToMany(Podcast::class)
    ->as('subscriptions')
    ->wherePivotNull('expired_at');

return $this->belongsToMany(Podcast::class)
    ->as('subscriptions')
    ->wherePivotNotNull('expired_at');

wherePivot 會在查詢中加入 where 子句限制,但在透過定義的關聯建立新模型時,並不會加入指定的數值。如果您需要同時查詢並在建立關聯時帶入特定的 pivot 數值,您可以使用 withPivotValue 方法:

php
return $this->belongsToMany(Role::class)
    ->withPivotValue('approved', 1);

透過中間表欄位排序查詢

您可以使用 orderByPivot 方法對 belongsToMany 關聯查詢所回傳的結果進行排序。在下方的範例中,我們將取得該使用者所有最新的徽章:

php
return $this->belongsToMany(Badge::class)
    ->where('rank', 'gold')
    ->orderByPivot('created_at', 'desc');

定義自定義中間表模型

如果您想定義一個自定義模型來代表多對多關聯的中間表,您可以在定義關聯時呼叫 using 方法。自定義 pivot 模型讓您有機會在 pivot 模型上定義額外的行為,例如方法與轉換 (Casts)。

自定義的多對多 pivot 模型應該繼承 Illuminate\Database\Eloquent\Relations\Pivot 類別,而自定義的多型多對多 pivot 模型則應該繼承 Illuminate\Database\Eloquent\Relations\MorphPivot 類別。例如,我們可以定義一個使用自定義 RoleUser pivot 模型的 Role 模型:

php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;

class Role extends Model
{
    /**
     * The users that belong to the role.
     */
    public function users(): BelongsToMany
    {
        return $this->belongsToMany(User::class)->using(RoleUser::class);
    }
}

定義 RoleUser 模型時,您應該繼承 Illuminate\Database\Eloquent\Relations\Pivot 類別:

php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Relations\Pivot;

class RoleUser extends Pivot
{
    // ...
}

⚠️ 警告

Pivot 模型不可使用 SoftDeletes trait。如果您需要軟刪除 pivot 紀錄,請考慮將您的 pivot 模型轉換為實際的 Eloquent 模型。

自定義 Pivot 模型與遞增 ID

如果您定義了一個使用自定義 pivot 模型的多對多關聯,且該 pivot 模型具有自動遞增的主鍵,則應確保您的自定義 pivot 模型類別中定義了一個設為 trueincrementing 屬性。

php
/**
 * Indicates if the IDs are auto-incrementing.
 *
 * @var bool
 */
public $incrementing = true;

多型關聯 (Polymorphic Relationships)

多型關聯 (Polymorphic Relationship) 允許子模型透過單一關聯隸屬於多種類型的模型。例如,想像您正在建立一個允許使用者分享部落格文章與影片的應用程式。在這樣的應用程式中,一個 Comment 模型可能同時隸屬於 PostVideo 模型。

一對一

資料表結構

一對一多型關聯類似於典型的一對一關聯;然而,子模型可以透過單一關聯隸屬於多種不同類型的模型。例如,部落格 PostUser 可能會共用一個指向 Image 模型的多型關聯。使用一對一多型關聯讓您可以擁有一個單一的唯一圖片資料表,並能與文章或使用者關聯。首先,讓我們來看看資料表結構:

text
posts
    id - integer
    name - string

users
    id - integer
    name - string

images
    id - integer
    url - string
    imageable_id - integer
    imageable_type - string

請注意 images 資料表中的 imageable_idimageable_type 欄位。imageable_id 欄位將包含文章或使用者的 ID 值,而 imageable_type 欄位則包含父模型的類別名稱。Eloquent 使用 imageable_type 欄位來決定在存取 imageable 關聯時該回傳哪種「類型」的父模型。在這個例子中,該欄位將包含 App\Models\PostApp\Models\User

模型結構

接下來,讓我們來看看建立此關聯所需的模型定義:

php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\MorphTo;

class Image extends Model
{
    /**
     * Get the parent imageable model (user or post).
     */
    public function imageable(): MorphTo
    {
        return $this->morphTo();
    }
}

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\MorphOne;

class Post extends Model
{
    /**
     * Get the post's image.
     */
    public function image(): MorphOne
    {
        return $this->morphOne(Image::class, 'imageable');
    }
}

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\MorphOne;

class User extends Model
{
    /**
     * Get the user's image.
     */
    public function image(): MorphOne
    {
        return $this->morphOne(Image::class, 'imageable');
    }
}

取得關聯

一旦定義好資料表與模型,您就可以透過模型來存取關聯。例如,要取得文章的圖片,我們可以存取 image 動態關聯屬性:

php
use App\Models\Post;

$post = Post::find(1);

$image = $post->image;

您可以透過存取呼叫 morphTo 的方法名稱來取得多型模型的父級。在此例中,即為 Image 模型上的 imageable 方法。因此,我們將該方法視為動態關聯屬性來存取:

php
use App\Models\Image;

$image = Image::find(1);

$imageable = $image->imageable;

Image 模型上的 imageable 關聯將回傳 PostUser 的執行個體,這取決於哪種類型的模型擁有該圖片。

鍵名慣例

如有必要,您可以指定多型子模型所使用的「id」與「type」欄位名稱。如果您這麼做,請確保務必將關聯名稱作為第一個參數傳遞給 morphTo 方法。通常,此值應與方法名稱相符,因此您可以使用 PHP 的 __FUNCTION__ 常數:

php
/**
 * Get the model that the image belongs to.
 */
public function imageable(): MorphTo
{
    return $this->morphTo(__FUNCTION__, 'imageable_type', 'imageable_id');
}

一對多

資料表結構

一對多多型關聯與典型的一對多關聯類似;然而,子模型可以透過單一關聯屬於多種類型的模型。例如,想像一下您應用程式的使用者可以對文章與影片進行「評論」。使用多型關聯,您可以使用單一 comments 資料表來包含文章與影片的評論。首先,讓我們看看建立此關聯所需的資料表結構:

text
posts
    id - integer
    title - string
    body - text

videos
    id - integer
    title - string
    url - string

comments
    id - integer
    body - text
    commentable_id - integer
    commentable_type - string

模型結構

接著,讓我們看看建立此關聯所需的模型定義:

php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\MorphTo;

class Comment extends Model
{
    /**
     * Get the parent commentable model (post or video).
     */
    public function commentable(): MorphTo
    {
        return $this->morphTo();
    }
}

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\MorphMany;

class Post extends Model
{
    /**
     * Get all of the post's comments.
     */
    public function comments(): MorphMany
    {
        return $this->morphMany(Comment::class, 'commentable');
    }
}

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\MorphMany;

class Video extends Model
{
    /**
     * Get all of the video's comments.
     */
    public function comments(): MorphMany
    {
        return $this->morphMany(Comment::class, 'commentable');
    }
}

取得關聯

一旦定義了資料庫表與模型,您就可以透過模型的動態關聯屬性存取這些關聯。例如,要存取文章的所有評論,我們可以使用 comments 動態屬性:

php
use App\Models\Post;

$post = Post::find(1);

foreach ($post->comments as $comment) {
    // ...
}

您也可以透過存取呼叫 morphTo 的方法名稱來取得多型子模型的父級。在這種情況下,即為 Comment 模型上的 commentable 方法。因此,我們將該方法作為動態關聯屬性進行存取,以取得評論的父級模型:

php
use App\Models\Comment;

$comment = Comment::find(1);

$commentable = $comment->commentable;

Comment 模型上的 commentable 關聯將回傳 PostVideo 實例,具體取決於哪種類型的模型是該評論的父級。

自動在子模型中載入父模型

即使使用 Eloquent 的積極載入 (Eager Loading),如果您在巡覽子模型時嘗試從子模型存取父模型,仍可能會出現「N + 1」查詢問題:

php
$posts = Post::with('comments')->get();

foreach ($posts as $post) {
    foreach ($post->comments as $comment) {
        echo $comment->commentable->title;
    }
}

在上面的範例中,引入了「N + 1」查詢問題,因為即使為每個 Post 模型積極載入了評論,Eloquent 也不會自動在每個子 Comment 模型上載入 (Hydrate) 父級 Post

如果您希望 Eloquent 自動將父模型載入到其子模型中,可以在定義 morphMany 關聯時呼叫 chaperone 方法:

php
class Post extends Model
{
    /**
     * Get all of the post's comments.
     */
    public function comments(): MorphMany
    {
        return $this->morphMany(Comment::class, 'commentable')->chaperone();
    }
}

或者,如果您想在執行時選擇啟用自動父級載入,可以在積極載入關聯時呼叫 chaperone 模型:

php
use App\Models\Post;

$posts = Post::with([
    'comments' => fn ($comments) => $comments->chaperone(),
])->get();

多個中的一個

有時一個模型可能有許多相關模型,但您希望輕鬆取得該關聯中「最新」或「最舊」的相關模型。例如,一個 User 模型可能與多個 Image 模型相關聯,但您想定義一種方便的方式與使用者上傳的最新圖片進行互動。您可以使用 morphOne 關聯類型結合 ofMany 方法來達成此目的:

php
/**
 * Get the user's most recent image.
 */
public function latestImage(): MorphOne
{
    return $this->morphOne(Image::class, 'imageable')->latestOfMany();
}

同樣地,您可以定義一個方法來取得關聯中「最舊」或第一個相關模型:

php
/**
 * Get the user's oldest image.
 */
public function oldestImage(): MorphOne
{
    return $this->morphOne(Image::class, 'imageable')->oldestOfMany();
}

預設情況下,latestOfManyoldestOfMany 方法將根據模型的遞增主鍵 (Primary Key) 取得最新或最舊的相關模型。然而,有時您可能希望使用不同的排序標準從較大的關聯中取得單一模型。

例如,使用 ofMany 方法,您可以取得使用者最受「喜愛」的圖片。ofMany 方法接受可排序的欄位作為其第一個參數,以及在查詢相關模型時要套用的聚合函式 (minmax) 作為第二個參數:

php
/**
 * Get the user's most popular image.
 */
public function bestImage(): MorphOne
{
    return $this->morphOne(Image::class, 'imageable')->ofMany('likes', 'max');
}

📌 備註

可以建構更進階的「多個中的一個」關聯。更多資訊請參閱 遠處多個中的一個說明文件

多對多

資料表結構

多型多對多關聯比「一對一」和「一對多」的多型關聯稍微複雜一點。例如,Post 模型和 Video 模型可以共享對 Tag 模型的多型關聯。在這種情況下使用多型多對多關聯,可以讓您的應用程式擁有一張不重複標籤的單一資料表,並與文章或影片關聯。首先,讓我們看看建立此關聯所需的資料表結構:

text
posts
    id - integer
    name - string

videos
    id - integer
    name - string

tags
    id - integer
    name - string

taggables
    tag_id - integer
    taggable_id - integer
    taggable_type - string

📌 備註

在深入探討多型多對多關聯之前,建議您先閱讀一般多對多關聯的說明文件。

模型結構

接著,我們準備好在模型上定義關聯。PostVideo 模型都將包含一個 tags 方法,該方法會呼叫基礎 Eloquent 模型類別提供的 morphToMany 方法。

morphToMany 方法接受相關模型類別的名稱以及「關聯名稱」。根據我們為中間表指定的名稱及其包含的鍵,我們將此關聯稱為 "taggable":

php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\MorphToMany;

class Post extends Model
{
    /**
     * Get all of the tags for the post.
     */
    public function tags(): MorphToMany
    {
        return $this->morphToMany(Tag::class, 'taggable');
    }
}

定義反向關聯

接著,在 Tag 模型中,您應該為其每個可能的父模型定義一個方法。因此,在這個範例中,我們將定義 posts 方法和 videos 方法。這兩個方法都應該回傳 morphedByMany 方法的結果。

morphedByMany 方法接受相關模型類別的名稱以及「關聯名稱」。根據我們為中間表指定的名稱及其包含的鍵,我們將此關聯稱為 "taggable":

php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\MorphToMany;

class Tag extends Model
{
    /**
     * Get all of the posts that are assigned this tag.
     */
    public function posts(): MorphToMany
    {
        return $this->morphedByMany(Post::class, 'taggable');
    }

    /**
     * Get all of the videos that are assigned this tag.
     */
    public function videos(): MorphToMany
    {
        return $this->morphedByMany(Video::class, 'taggable');
    }
}

取出關聯

一旦您的資料庫資料表和模型定義完成,您就可以透過模型存取這些關聯。例如,要存取文章的所有標籤,您可以使用 tags 動態關聯屬性:

php
use App\Models\Post;

$post = Post::find(1);

foreach ($post->tags as $tag) {
    // ...
}

您可以從多型子模型中,透過存取呼叫 morphedByMany 的方法名稱來取出多型關聯的父級模型。在這個範例中,即是 Tag 模型上的 postsvideos 方法:

php
use App\Models\Tag;

$tag = Tag::find(1);

foreach ($tag->posts as $post) {
    // ...
}

foreach ($tag->videos as $video) {
    // ...
}

自定義多型類型

預設情況下,Laravel 會使用完整格式的類別名稱(Fully Qualified Class Name)來儲存相關模型的「類型」。例如,在上述的一對多關聯範例中,當 Comment 模型可能屬於 PostVideo 模型時,預設的 commentable_type 將分別為 App\Models\PostApp\Models\Video。然而,您可能希望將這些值與應用程式的內部結構解耦。

例如,我們可以使用簡單的字串如 postvideo 來代替模型名稱作為「類型」。藉由這樣做,即使模型被重新命名,資料庫中多型「類型」欄位的值仍將保持有效:

php
use Illuminate\Database\Eloquent\Relations\Relation;

Relation::enforceMorphMap([
    'post' => 'App\Models\Post',
    'video' => 'App\Models\Video',
]);

您可以在 App\Providers\AppServiceProvider 類別的 boot 方法中呼叫 enforceMorphMap 方法,或者如果您願意,也可以建立一個獨立的服務提供者。

您可以在執行期間使用模型的 getMorphClass 方法來取得給定模型的多型別名。相反地,您可以使用 Relation::getMorphedModel 方法來取得與多型別名關聯的完整類別名稱:

php
use Illuminate\Database\Eloquent\Relations\Relation;

$alias = $post->getMorphClass();

$class = Relation::getMorphedModel($alias);

⚠️ 警告

當為現有的應用程式新增「morph map」時,資料庫中每個仍包含完整類別名稱的多型 *_type 欄位值都需要轉換為其「對應地圖(Map)」名稱。

動態關聯

您可以使用 resolveRelationUsing 方法在執行期間定義 Eloquent 模型之間的關聯。雖然通常不建議在一般應用程式開發中使用,但在開發 Laravel 套件時,這有時會很有用。

resolveRelationUsing 方法接受所需的關聯名稱作為其第一個參數。傳遞給該方法的第二個參數應該是一個閉包,該閉包接受模型實例並回傳一個有效的 Eloquent 關聯定義。通常,您應該在服務提供者的 boot 方法中設定動態關聯:

php
use App\Models\Order;
use App\Models\Customer;

Order::resolveRelationUsing('customer', function (Order $orderModel) {
    return $orderModel->belongsTo(Customer::class, 'customer_id');
});

⚠️ 警告

當定義動態關聯時,請務必為 Eloquent 關聯方法提供明確的鍵名參數。

查詢關聯

由於所有的 Eloquent 關聯都是透過方法定義的,因此你可以呼叫這些方法來取得關聯的實例,而無需實際執行查詢來載入相關模型。此外,所有類型的 Eloquent 關聯也同時作為 查詢產生器 (Query Builders) 使用,讓你能在最終對資料庫執行 SQL 查詢之前,繼續在關聯查詢上串接限制條件。

例如,假設有一個部落格應用程式,其中 User 模型有許多關聯的 Post 模型:

php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;

class User extends Model
{
    /**
     * Get all of the posts for the user.
     */
    public function posts(): HasMany
    {
        return $this->hasMany(Post::class);
    }
}

你可以查詢 posts 關聯並為該關聯添加額外的限制,如下所示:

php
use App\Models\User;

$user = User::find(1);

$user->posts()->where('active', 1)->get();

你可以在關聯上使用任何 Laravel 查詢產生器 的方法,因此請務必閱讀查詢產生器的文件,以了解所有可用的方法。

在關聯後串接 orWhere 子句

如上方範例所示,你在查詢關聯時可以自由地添加額外的限制。然而,在關聯後串接 orWhere 子句時請務必小心,因為 orWhere 子句在邏輯上會與關聯限制處於同一層級:

php
$user->posts()
    ->where('active', 1)
    ->orWhere('votes', '>=', 100)
    ->get();

上方的範例將產生以下 SQL。如你所見,or 子句指示查詢回傳 任何 票數大於等於 100 的文章。該查詢不再受限於特定使用者:

sql
select *
from posts
where user_id = ? and active = 1 or votes >= 100

在大多數情況下,你應該使用 邏輯分組 將條件檢查放在括號內:

php
use Illuminate\Database\Eloquent\Builder;

$user->posts()
    ->where(function (Builder $query) {
        return $query->where('active', 1)
            ->orWhere('votes', '>=', 100);
    })
    ->get();

上方的範例將產生以下 SQL。請注意,邏輯分組已正確地將限制條件歸類在一起,且查詢依然受限於特定使用者:

sql
select *
from posts
where user_id = ? and (active = 1 or votes >= 100)

關聯方法與動態屬性

如果你不需要為 Eloquent 關聯查詢添加額外的限制,你可以像存取屬性一樣存取該關聯。例如,延續我們的 UserPost 範例模型,我們可以像這樣存取使用者的所有文章:

php
use App\Models\User;

$user = User::find(1);

foreach ($user->posts as $post) {
    // ...
}

動態關聯屬性執行的是「延遲載入 (Lazy Loading)」,這意味著只有在你實際存取這些屬性時,它們才會載入關聯資料。正因如此,開發者通常會使用 積極載入 (Eager Loading) 來預先載入他們已知在載入模型後會被存取的關聯。積極載入能顯著減少載入模型關聯時必須執行的 SQL 查詢次數。

查詢關聯是否存在

在取得模型紀錄時,你可能希望根據關聯的存在與否來限制結果。例如,假設你想取得所有至少有一則留言的部落格文章。為此,你可以將關聯名稱傳遞給 hasorHas 方法:

php
use App\Models\Post;

// Retrieve all posts that have at least one comment...
$posts = Post::has('comments')->get();

你也可以指定運算子和計數值來進一步自定義查詢:

php
// Retrieve all posts that have three or more comments...
$posts = Post::has('comments', '>=', 3)->get();

巢狀的 has 語句可以使用「點」號表示法來建構。例如,你可以取得所有至少有一則留言,且該留言至少有一張圖片的文章:

php
// Retrieve posts that have at least one comment with images...
$posts = Post::has('comments.images')->get();

如果你需要更強大的功能,可以使用 whereHasorWhereHas 方法在你的 has 查詢中定義額外的查詢限制,例如檢查留言的內容:

php
use Illuminate\Database\Eloquent\Builder;

// Retrieve posts with at least one comment containing words like code%...
$posts = Post::whereHas('comments', function (Builder $query) {
    $query->where('content', 'like', 'code%');
})->get();

// Retrieve posts with at least ten comments containing words like code%...
$posts = Post::whereHas('comments', function (Builder $query) {
    $query->where('content', 'like', 'code%');
}, '>=', 10)->get();

⚠️ 警告

Eloquent 目前不支援跨資料庫的關聯存在性查詢。關聯必須存在於同一個資料庫中。

多對多關聯存在性查詢

whereAttachedTo 方法可用於查詢那些與特定模型或模型集合具有多對多關聯的模型:

php
$users = User::whereAttachedTo($role)->get();

你也可以將 集合 (Collection) 實例傳遞給 whereAttachedTo 方法。這樣做時,Laravel 將取得與集合內任何父模型相關聯的模型:

php
$tags = Tag::whereLike('name', '%laravel%')->get();

$posts = Post::whereAttachedTo($tags)->get();

行內關聯存在性查詢

如果你想在關聯查詢中使用單一、簡單的 where 條件來查詢關聯的存在性,使用 whereRelationorWhereRelationwhereMorphRelation 以及 orWhereMorphRelation 方法可能會更方便。例如,我們可以查詢所有具有未核准留言的文章:

php
use App\Models\Post;

$posts = Post::whereRelation('comments', 'is_approved', false)->get();

當然,就像呼叫查詢產生器的 where 方法一樣,你也可以指定一個運算子:

php
$posts = Post::whereRelation(
    'comments', 'created_at', '>=', now()->minus(hours: 1)
)->get();

查詢關聯是否不存在

在取得模型紀錄時,你可能希望根據關聯的缺失與否來限制結果。例如,假設你想取得所有沒有任何留言的部落格文章。為此,你可以將關聯名稱傳遞給 doesntHaveorDoesntHave 方法:

php
use App\Models\Post;

$posts = Post::doesntHave('comments')->get();

如果你需要更強大的功能,可以使用 whereDoesntHaveorWhereDoesntHave 方法為你的 doesntHave 查詢添加額外的查詢限制,例如檢查留言的內容:

php
use Illuminate\Database\Eloquent\Builder;

$posts = Post::whereDoesntHave('comments', function (Builder $query) {
    $query->where('content', 'like', 'code%');
})->get();

你可以使用「點」號表示法對巢狀關聯執行查詢。例如,以下查詢將取得所有沒有留言的文章,以及雖然有留言但留言者皆非被封鎖使用者的文章:

php
use Illuminate\Database\Eloquent\Builder;

$posts = Post::whereDoesntHave('comments.author', function (Builder $query) {
    $query->where('banned', 1);
})->get();

查詢 Morph To 關聯

若要查詢「morph to」關聯是否存在,您可以使用 whereHasMorphwhereDoesntHaveMorph 方法。這些方法接受關聯名稱作為其第一個參數。接著,這些方法接受您希望包含在查詢中的相關模型名稱。最後,您可以提供一個閉包來定義自定義的關聯查詢:

php
use App\Models\Comment;
use App\Models\Post;
use App\Models\Video;
use Illuminate\Database\Eloquent\Builder;

// Retrieve comments associated to posts or videos with a title like code%...
$comments = Comment::whereHasMorph(
    'commentable',
    [Post::class, Video::class],
    function (Builder $query) {
        $query->where('title', 'like', 'code%');
    }
)->get();

// Retrieve comments associated to posts with a title not like code%...
$comments = Comment::whereDoesntHaveMorph(
    'commentable',
    Post::class,
    function (Builder $query) {
        $query->where('title', 'like', 'code%');
    }
)->get();

有時您可能需要根據相關多型模型的「類型 (Type)」來增加查詢限制。傳遞給 whereHasMorph 方法的閉包可以接收 $type 值作為其第二個參數。此參數允許您檢查正在建立的查詢「類型」:

php
use Illuminate\Database\Eloquent\Builder;

$comments = Comment::whereHasMorph(
    'commentable',
    [Post::class, Video::class],
    function (Builder $query, string $type) {
        $column = $type === Post::class ? 'content' : 'title';

        $query->where($column, 'like', 'code%');
    }
)->get();

有時您可能想要查詢「morph to」關聯之父級的子模型。您可以使用 whereMorphedTowhereNotMorphedTo 方法來達成此目的,這些方法會自動為給定的模型決定正確的多型類型對應。這些方法接受 morphTo 關聯的名稱作為其第一個參數,並接受相關的父級模型作為其第二個參數:

php
$comments = Comment::whereMorphedTo('commentable', $post)
    ->orWhereMorphedTo('commentable', $video)
    ->get();

查詢所有相關模型

您可以提供 * 作為通配字元值,而不是傳遞一個可能的多型模型陣列。這會指示 Laravel 從資料庫中取出所有可能的多型類型。Laravel 將執行一個額外的查詢以執行此操作:

php
use Illuminate\Database\Eloquent\Builder;

$comments = Comment::whereHasMorph('commentable', '*', function (Builder $query) {
    $query->where('title', 'like', 'foo%');
})->get();

聚合相關模型

計算相關模型數量

有時你可能想在不實際載入模型的情況下,計算給定關聯的相關模型數量。若要達成此目的,你可以使用 withCount 方法。withCount 方法會在結果模型上放置一個 {relation}_count 屬性:

php
use App\Models\Post;

$posts = Post::withCount('comments')->get();

foreach ($posts as $post) {
    echo $post->comments_count;
}

透過向 withCount 方法傳遞一個陣列,你可以增加多個關聯的「計數」,也可以為查詢增加額外的約束:

php
use Illuminate\Database\Eloquent\Builder;

$posts = Post::withCount(['votes', 'comments' => function (Builder $query) {
    $query->where('content', 'like', 'code%');
}])->get();

echo $posts[0]->votes_count;
echo $posts[0]->comments_count;

你也可以為關聯計數結果設定別名,從而允許對同一個關聯進行多次計數:

php
use Illuminate\Database\Eloquent\Builder;

$posts = Post::withCount([
    'comments',
    'comments as pending_comments_count' => function (Builder $query) {
        $query->where('approved', false);
    },
])->get();

echo $posts[0]->comments_count;
echo $posts[0]->pending_comments_count;

延遲計數載入

使用 loadCount 方法,你可以在父模型已經被取出後再載入關聯計數:

php
$book = Book::first();

$book->loadCount('genres');

如果你需要為計數查詢設置額外的查詢約束,可以傳遞一個以你希望計數的關聯為鍵的陣列。陣列的值應為接收查詢產生器執行個體的閉包:

php
$book->loadCount(['reviews' => function (Builder $query) {
    $query->where('rating', 5);
}])

關聯計數與自定義 Select 語句

如果你將 withCountselect 語句結合使用,請確保你在 select 方法之後才呼叫 withCount

php
$posts = Post::select(['title', 'body'])
    ->withCount('comments')
    ->get();

其他聚合函式

除了 withCount 方法,Eloquent 還提供了 withMinwithMaxwithAvgwithSum 以及 withExists 方法。這些方法會在你的結果模型上放置一個 {relation}_{function}_{column} 屬性:

php
use App\Models\Post;

$posts = Post::withSum('comments', 'votes')->get();

foreach ($posts as $post) {
    echo $post->comments_sum_votes;
}

如果你希望使用其他名稱來存取聚合函式的結果,可以指定你自己的別名:

php
$posts = Post::withSum('comments as total_comments', 'votes')->get();

foreach ($posts as $post) {
    echo $post->total_comments;
}

loadCount 方法類似,這些方法也提供延遲版本。這些額外的聚合操作可以在已經取出的 Eloquent 模型上執行:

php
$post = Post::first();

$post->loadSum('comments', 'votes');

如果你將這些聚合方法與 select 語句結合使用,請確保你在 select 方法之後才呼叫這些聚合方法:

php
$posts = Post::select(['title', 'body'])
    ->withExists('comments')
    ->get();

計算 Morph To 關聯的模型數量

如果你想積極載入一個「morph to」關聯,以及該關聯可能回傳的各種實體的相關模型計數,你可以結合使用 with 方法與 morphTo 關聯的 morphWithCount 方法。

在此範例中,讓我們假設 PhotoPost 模型都可以建立 ActivityFeed 模型。我們假設 ActivityFeed 模型定義了一個名為 parentable 的「morph to」關聯,讓我們能取出給定 ActivityFeed 執行個體的父級 PhotoPost 模型。此外,假設 Photo 模型「擁有多個」Tag 模型,而 Post 模型「擁有多個」Comment 模型。

現在,想像我們想要取出 ActivityFeed 執行個體,並為每個 ActivityFeed 執行個體積極載入 parentable 父模型。此外,我們想要取出與每個父級相片關聯的標籤數量,以及與每個父級文章關聯的留言數量:

php
use Illuminate\Database\Eloquent\Relations\MorphTo;

$activities = ActivityFeed::with([
    'parentable' => function (MorphTo $morphTo) {
        $morphTo->morphWithCount([
            Photo::class => ['tags'],
            Post::class => ['comments'],
        ]);
    }])->get();

延遲計數載入

假設我們已經取出了一組 ActivityFeed 模型,現在我們想要載入與這些動態摘要關聯的各種 parentable 模型的巢狀關聯計數。你可以使用 loadMorphCount 方法來達成:

php
$activities = ActivityFeed::with('parentable')->get();

$activities->loadMorphCount('parentable', [
    Photo::class => ['tags'],
    Post::class => ['comments'],
]);

積極載入 (Eager Loading)

當將 Eloquent 關聯作為屬性存取時,相關模型是「延遲載入 (Lazy Loaded)」的。這意味著在您第一次存取該屬性之前,關聯資料實際上並未被載入。然而,Eloquent 可以在查詢父模型時「積極載入 (Eager Load)」關聯。積極載入緩解了「N + 1」查詢問題。為了說明 N + 1 查詢問題,請考慮一個「屬於 (belongsTo)」Author 模型的 Book 模型:

php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;

class Book extends Model
{
    /**
     * Get the author that wrote the book.
     */
    public function author(): BelongsTo
    {
        return $this->belongsTo(Author::class);
    }
}

現在,讓我們取得所有書籍及其作者:

php
use App\Models\Book;

$books = Book::all();

foreach ($books as $book) {
    echo $book->author->name;
}

此迴圈將執行一次查詢以取得資料表中的所有書籍,然後為每本書執行另一次查詢以取得該書的作者。因此,如果我們有 25 本書,上述程式碼將執行 26 次查詢:一次查詢原始書籍,以及 25 次額外查詢以取得每本書的作者。

幸運的是,我們可以使用積極載入將此操作減少到僅兩次查詢。在建立查詢時,您可以使用 with 方法指定哪些關聯應該被積極載入:

php
$books = Book::with('author')->get();

foreach ($books as $book) {
    echo $book->author->name;
}

對於此操作,僅會執行兩次查詢——一次查詢以取得所有書籍,一次查詢以取得所有書籍的所有作者:

sql
select * from books

select * from authors where id in (1, 2, 3, 4, 5, ...)

積極載入多個關聯

有時您可能需要積極載入多個不同的關聯。為此,只需將關聯陣列傳遞給 with 方法即可:

php
$books = Book::with(['author', 'publisher'])->get();

巢狀積極載入

若要積極載入關聯中的關聯,您可以使用「點號 (dot)」語法。例如,讓我們積極載入書籍的所有作者以及作者的所有個人聯絡人:

php
$books = Book::with('author.contacts')->get();

或者,您可以透過向 with 方法提供巢狀陣列來指定巢狀積極載入關聯,這在積極載入多個巢狀關聯時非常方便:

php
$books = Book::with([
    'author' => [
        'contacts',
        'publisher',
    ],
])->get();

巢狀積極載入 morphTo 關聯

如果您想積極載入 morphTo 關聯,以及該關聯可能回傳的各種實體上的巢狀關聯,您可以將 with 方法與 morphTo 關聯的 morphWith 方法結合使用。為了幫助說明此方法,讓我們考慮以下模型:

php
<?php

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\MorphTo;

class ActivityFeed extends Model
{
    /**
     * Get the parent of the activity feed record.
     */
    public function parentable(): MorphTo
    {
        return $this->morphTo();
    }
}

在此範例中,讓我們假設 EventPhotoPost 模型可能會建立 ActivityFeed 模型。此外,假設 Event 模型屬於 Calendar 模型,Photo 模型與 Tag 模型相關聯,而 Post 模型屬於 Author 模型。

使用這些模型定義和關聯,我們可以取得 ActivityFeed 模型實例,並積極載入所有 parentable 模型及其各自的巢狀關聯:

php
use Illuminate\Database\Eloquent\Relations\MorphTo;

$activities = ActivityFeed::query()
    ->with(['parentable' => function (MorphTo $morphTo) {
        $morphTo->morphWith([
            Event::class => ['calendar'],
            Photo::class => ['tags'],
            Post::class => ['author'],
        ]);
    }])->get();

積極載入指定欄位

您可能並不總是需要從正在取得的關聯中取得每個欄位。因此,Eloquent 允許您指定想要取出的關聯欄位:

php
$books = Book::with('author:id,name,book_id')->get();

⚠️ 警告

使用此功能時,您應始終在要取出的欄位清單中包含 id 欄位和任何相關的外鍵欄位。

預設積極載入

有時您可能希望在取得模型時始終載入某些關聯。為此,您可以在模型上定義 $with 屬性:

php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;

class Book extends Model
{
    /**
     * The relationships that should always be loaded.
     *
     * @var array
     */
    protected $with = ['author'];

    /**
     * Get the author that wrote the book.
     */
    public function author(): BelongsTo
    {
        return $this->belongsTo(Author::class);
    }

    /**
     * Get the genre of the book.
     */
    public function genre(): BelongsTo
    {
        return $this->belongsTo(Genre::class);
    }
}

如果您想在單個查詢中從 $with 屬性中移除某個項目,可以使用 without 方法:

php
$books = Book::without('author')->get();

如果您想在單個查詢中覆蓋 $with 屬性中的所有項目,可以使用 withOnly 方法:

php
$books = Book::withOnly('genre')->get();

限制積極載入

有時您可能希望在積極載入關聯的同時,也為該積極載入查詢指定額外的查詢條件。您可以透過向 with 方法傳遞一個陣列來實現,其中陣列的鍵是關聯名稱,值則是為積極載入查詢添加額外限制條件的閉包:

php
use App\Models\User;

$users = User::with(['posts' => function ($query) {
    $query->where('title', 'like', '%code%');
}])->get();

在此範例中,Eloquent 只會積極載入貼文的 title 欄位包含單字 code 的貼文。您可以呼叫其他 查詢產生器 (query builder) 的方法來進一步自定義積極載入操作:

php
$users = User::with(['posts' => function ($query) {
    $query->orderBy('created_at', 'desc');
}])->get();

限制 morphTo 關聯的積極載入

如果您正在積極載入一個 morphTo 關聯,Eloquent 會執行多個查詢來獲取每種類型的相關模型。您可以使用 MorphTo 關聯的 constrain 方法為這些查詢中的每一個添加額外的限制條件:

php
use Illuminate\Database\Eloquent\Relations\MorphTo;

$comments = Comment::with(['commentable' => function (MorphTo $morphTo) {
    $morphTo->constrain([
        Post::class => function ($query) {
            $query->whereNull('hidden_at');
        },
        Video::class => function ($query) {
            $query->where('type', 'educational');
        },
    ]);
}])->get();

在此範例中,Eloquent 將只會積極載入未被隱藏的貼文以及 type 值為 "educational" 的影片。

透過關聯是否存在來限制積極載入

有時您可能會發現自己需要檢查關聯是否存在,同時根據相同的條件載入該關聯。例如,您可能只想取得具有符合特定查詢條件的子 Post 模型的 User 模型,同時積極載入這些符合條件的貼文。您可以使用 withWhereHas 方法來達成此目的:

php
use App\Models\User;

$users = User::withWhereHas('posts', function ($query) {
    $query->where('featured', true);
})->get();

延遲積極載入 (Lazy Eager Loading)

有時您可能需要在取得父模型後才積極載入關聯。例如,如果您需要動態決定是否載入相關模型,這會非常有用:

php
use App\Models\Book;

$books = Book::all();

if ($condition) {
    $books->load('author', 'publisher');
}

如果您需要在積極載入查詢上設定額外的查詢限制條件,您可以傳遞一個以您希望載入的關聯為鍵的陣列。陣列的值應該是接收查詢實例的閉包實例:

php
$author->load(['books' => function ($query) {
    $query->orderBy('published_date', 'asc');
}]);

若要僅在關聯尚未載入時才載入它,請使用 loadMissing 方法:

php
$book->loadMissing('author');

巢狀延遲積極載入與 morphTo

如果您想積極載入 morphTo 關聯,以及該關聯可能回傳的各種實體上的巢狀關聯,您可以使用 loadMorph 方法。

此方法的第一個參數接受 morphTo 關聯的名稱,第二個參數接受模型 / 關聯對的陣列。為了幫助說明此方法,讓我們考慮以下模型:

php
<?php

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\MorphTo;

class ActivityFeed extends Model
{
    /**
     * Get the parent of the activity feed record.
     */
    public function parentable(): MorphTo
    {
        return $this->morphTo();
    }
}

在此範例中,讓我們假設 EventPhotoPost 模型可能會建立 ActivityFeed 模型。此外,假設 Event 模型屬於 Calendar 模型,Photo 模型與 Tag 模型相關聯,而 Post 模型屬於 Author 模型。

使用這些模型定義和關聯,我們可以取得 ActivityFeed 模型實例,並積極載入所有 parentable 模型及其各自的巢狀關聯:

php
$activities = ActivityFeed::with('parentable')
    ->get()
    ->loadMorph('parentable', [
        Event::class => ['calendar'],
        Photo::class => ['tags'],
        Post::class => ['author'],
    ]);

自動積極載入

⚠️ 警告

此功能目前處於測試階段,以收集社群回饋。此功能的行為和功能甚至可能在修補版本 (patch releases) 中發生變化。

在許多情況下,Laravel 可以自動積極載入您存取的關聯。若要啟用自動積極載入,您應該在應用程式 AppServiceProviderboot 方法中呼叫 Model::automaticallyEagerLoadRelationships 方法:

php
use Illuminate\Database\Eloquent\Model;

/**
 * Bootstrap any application services.
 */
public function boot(): void
{
    Model::automaticallyEagerLoadRelationships();
}

啟用此功能後,Laravel 將嘗試自動載入您存取的任何先前尚未載入的關聯。例如,考慮以下情境:

php
use App\Models\User;

$users = User::all();

foreach ($users as $user) {
    foreach ($user->posts as $post) {
        foreach ($post->comments as $comment) {
            echo $comment->content;
        }
    }
}

通常,上述程式碼會為每個使用者執行一個查詢以取得其貼文,並為每篇貼文執行一個查詢以取得其留言。然而,當 automaticallyEagerLoadRelationships 功能啟用時,當您嘗試存取任何已取得使用者的貼文時,Laravel 將自動為使用者集合中的所有使用者 延遲積極載入 (lazy eager load) 其貼文。同樣地,當您嘗試存取任何已取得貼文的留言時,所有留言都將為最初取得的所有貼文進行延遲積極載入。

如果您不想全域啟用自動積極載入,您仍然可以透過在集合上呼叫 withRelationshipAutoloading 方法,為單個 Eloquent 集合實例啟用此功能:

php
$users = User::where('vip', true)->get();

return $users->withRelationshipAutoloading();

防止延遲載入

如前所述,積極載入 (Eager Loading) 關聯通常能為您的應用程式帶來顯著的效能提升。因此,如果您願意,您可以指示 Laravel 始終防止延遲載入 (Lazy Loading) 關聯。要達成此目的,您可以呼叫基礎 Eloquent 模型類別提供的 preventLazyLoading 方法。通常,您應該在應用程式 AppServiceProvider 類別的 boot 方法中呼叫此方法。

preventLazyLoading 方法接受一個選填的布林值參數,用來指示是否應防止延遲載入。例如,您可能只想在非正式環境中停用延遲載入,這樣即使在正式環境程式碼中意外出現了延遲載入關聯,您的正式環境仍能正常運作:

php
use Illuminate\Database\Eloquent\Model;

/**
 * Bootstrap any application services.
 */
public function boot(): void
{
    Model::preventLazyLoading(! $this->app->isProduction());
}

防止延遲載入後,當您的應用程式嘗試延遲載入任何 Eloquent 關聯時,Eloquent 將會拋出一個 Illuminate\Database\LazyLoadingViolationException 例外。

您可以使用 handleLazyLoadingViolationsUsing 方法來自定義延遲載入違規的行為。例如,透過此方法,您可以指示僅記錄延遲載入違規,而不是以例外中斷應用程式的執行:

php
Model::handleLazyLoadingViolationUsing(function (Model $model, string $relation) {
    $class = $model::class;

    info("Attempted to lazy load [{$relation}] on model [{$class}].");
});

新增與更新相關模型

save 方法

Eloquent 提供了為關聯新增模型的多種簡便方法。例如,你也許需要為一個 Post 新增一則 Comment。與其手動設定 Comment 模型上的 post_id 屬性,你可以直接從關聯使用 save 方法來新增 Comment:

php
use App\Models\Comment;
use App\Models\Post;

$comment = new Comment(['message' => 'A new comment.']);

$post = Post::find(1);

$post->comments()->save($comment);

注意,我們沒有像存取動態屬性那樣存取 comments 關聯。相反地,我們呼叫了 comments 方法來取得該關聯的實例。save 方法會自動將適當的 post_id 值加入到新的 Comment 模型中。

如果您需要儲存多個相關模型,您可以使用 saveMany 方法:

php
$post = Post::find(1);

$post->comments()->saveMany([
    new Comment(['message' => 'A new comment.']),
    new Comment(['message' => 'Another new comment.']),
]);

savesaveMany 方法會持久化給定的模型實例,但不會將新持久化的模型加入到已經載入至父模型記憶體中的關聯中。如果您打算在執行 savesaveMany 方法後存取該關聯,您可能希望使用 refresh 方法來重新載入該模型及其關聯:

php
$post->comments()->save($comment);

$post->refresh();

// All comments, including the newly saved comment...
$post->comments;

遞迴地儲存模型與關聯

如果您想要 save 您的模型及其所有相關的關聯,您可以使用 push 方法。在此範例中,Post 模型及其評論,以及評論的作者都會被儲存:

php
$post = Post::find(1);

$post->comments[0]->message = 'Message';
$post->comments[0]->author->name = 'Author Name';

$post->push();

pushQuietly 方法可用於儲存模型及其關聯,且不引發任何事件:

php
$post->pushQuietly();

create 方法

除了 savesaveMany 方法之外,您也可以使用 create 方法。它接受屬性的陣列,建立模型並將其插入資料庫中。savecreate 之間的區別在於 save 接受一個完整的 Eloquent 模型實例,而 create 則接受一個純 PHP 的 arraycreate 方法會回傳新建立的模型:

php
use App\Models\Post;

$post = Post::find(1);

$comment = $post->comments()->create([
    'message' => 'A new comment.',
]);

您可以使用 createMany 方法來建立多個相關模型:

php
$post = Post::find(1);

$post->comments()->createMany([
    ['message' => 'A new comment.'],
    ['message' => 'Another new comment.'],
]);

createQuietlycreateManyQuietly 方法可用於在不分發任何事件的情況下建立一個或多個模型:

php
$user = User::find(1);

$user->posts()->createQuietly([
    'title' => 'Post title.',
]);

$user->posts()->createManyQuietly([
    ['title' => 'First post.'],
    ['title' => 'Second post.'],
]);

您也可以使用 findOrNewfirstOrNewfirstOrCreateupdateOrCreate 方法來建立與更新關聯上的模型

📌 備註

在使用 create 方法之前,請務必查看 批量賦值 (Mass Assignment) 說明文件。

Belongs To 關聯

如果您想將子模型分配給新的父模型,您可以使用 associate 方法。在此範例中,User 模型定義了一個與 Account 模型的 belongsTo 關聯。associate 方法會設定子模型上的外鍵:

php
use App\Models\Account;

$account = Account::find(10);

$user->account()->associate($account);

$user->save();

要將父模型從子模型中移除,您可以使用 dissociate 方法。此方法會將該關聯的外鍵設定為 null

php
$user->account()->dissociate();

$user->save();

多對多關聯

附加 / 卸載

Eloquent 也提供了方法讓處理多對多關聯更加方便。舉例來說,想像一個使用者可以擁有多個角色,而一個角色可以由多個使用者共享。您可以使用 attach 方法透過在關聯的中間表插入一筆記錄,將角色附加給使用者:

php
use App\Models\User;

$user = User::find(1);

$user->roles()->attach($roleId);

將關聯附加到模型時,您也可以傳遞額外資料的陣列,以便插入到中間表中:

php
$user->roles()->attach($roleId, ['expires' => $expires]);

有時候可能有必要從使用者身上移除某個角色。要移除多對多關聯的記錄,請使用 detach 方法。detach 方法會從中間表中刪除對應的記錄;然而,兩個模型都將保留在資料庫中:

php
// Detach a single role from the user...
$user->roles()->detach($roleId);

// Detach all roles from the user...
$user->roles()->detach();

為了方便起見,attachdetach 也接受 ID 陣列作為輸入:

php
$user = User::find(1);

$user->roles()->detach([1, 2, 3]);

$user->roles()->attach([
    1 => ['expires' => $expires],
    2 => ['expires' => $expires],
]);

同步關聯

您也可以使用 sync 方法來建構多對多關聯。sync 方法接受一個 ID 陣列放置在中間表。不在給定陣列中的任何 ID 都將從中間表中移除。因此,此操作完成後,只有給定陣列中的 ID 會存在於中間表中:

php
$user->roles()->sync([1, 2, 3]);

您也可以連同 ID 一併傳遞額外的中間表欄位值:

php
$user->roles()->sync([1 => ['expires' => true], 2, 3]);

如果您想為每個同步的模型 ID 插入相同的中間表欄位值,您可以使用 syncWithPivotValues 方法:

php
$user->roles()->syncWithPivotValues([1, 2, 3], ['active' => true]);

如果您不想卸載在給定陣列中遺漏的現有 ID,您可以使用 syncWithoutDetaching 方法:

php
$user->roles()->syncWithoutDetaching([1, 2, 3]);

切換關聯

多對多關聯也提供了一個 toggle 方法,它可以「切換」給定相關模型 ID 的附加狀態。如果給定的 ID 目前已附加,它將被卸載。同樣地,如果它目前是卸載狀態,它將會被附加:

php
$user->roles()->toggle([1, 2, 3]);

您也可以連同 ID 一併傳遞額外的中間表欄位值:

php
$user->roles()->toggle([
    1 => ['expires' => true],
    2 => ['expires' => true],
]);

更新中間表上的記錄

如果您需要更新關聯中間表中的現有資料列,您可以使用 updateExistingPivot 方法。此方法接受中間表記錄的外鍵以及要更新的屬性陣列:

php
$user = User::find(1);

$user->roles()->updateExistingPivot($roleId, [
    'active' => false,
]);

更新父級時間戳記 (Touching Parent Timestamps)

當一個模型定義了 belongsTobelongsToMany 關聯到另一個模型時,例如一個屬於 PostComment,當子模型更新時,有時更新父模型的時間戳記會很有幫助。

例如,當 Comment 模型更新時,您可能希望自動「更新 (touch)」所屬 Postupdated_at 時間戳記,使其設為目前的日期與時間。要達成此目的,您可以在子模型中加入一個 touches 屬性,其中包含在子模型更新時應一併更新其 updated_at 時間戳記的關聯名稱:

php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;

class Comment extends Model
{
    /**
     * All of the relationships to be touched.
     *
     * @var array
     */
    protected $touches = ['post'];

    /**
     * Get the post that the comment belongs to.
     */
    public function post(): BelongsTo
    {
        return $this->belongsTo(Post::class);
    }
}

⚠️ 警告

只有在使用 Eloquent 的 save 方法更新子模型時,才會更新父模型的時間戳記。