跳到內容

Eloquent:關聯

簡介

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

定義關聯

Eloquent 關聯定義為 Eloquent 模型類別上的方法。由於關聯也作為強大的 查詢建構器,將關聯定義為方法可以提供強大的方法鏈接和查詢功能。例如,我們可以在此 posts 關聯上鏈接其他查詢限制

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

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

一對一 / Has One

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

<?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 的動態屬性來檢索相關記錄。動態屬性允許您像存取模型上定義的屬性一樣存取關聯方法

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

Eloquent 會根據父模型名稱判斷關聯的外鍵。在這種情況下,會自動假設 Phone 模型具有 user_id 外鍵。如果您希望覆寫此慣例,您可以將第二個引數傳遞給 hasOne 方法

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

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

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

定義關聯的反向關係

因此,我們可以從 User 模型存取 Phone 模型。接下來,讓我們在 Phone 模型上定義一個關聯,讓我們可以存取擁有電話的使用者。我們可以使用 belongsTo 方法定義 hasOne 關聯的反向關係

<?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 方法

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

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

/**
* 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
 
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 會採用父模型的「蛇形命名」名稱,並在其後加上 _id。因此,在此範例中,Eloquent 會假設 Comment 模型上的外鍵欄位為 post_id

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

use App\Models\Post;
 
$comments = Post::find(1)->comments;
 
foreach ($comments as $comment) {
// ...
}

由於所有關聯也作為查詢建構器,您可以透過呼叫 comments 方法並繼續將條件鏈接到查詢,進一步限制關聯查詢

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

hasOne 方法一樣,您也可以透過將其他引數傳遞給 hasMany 方法來覆寫外鍵和本機鍵

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

自動為子模型補充父模型

即使使用 Eloquent 預先載入 (eager loading),如果你在迴圈遍歷子模型時嘗試存取父模型,仍可能發生「N + 1」查詢問題

$posts = Post::with('comments')->get();
 
foreach ($posts as $post) {
foreach ($post->comments as $comment) {
echo $comment->post->title;
}
}

在上面的範例中,因為即使 `Post` 模型已預先載入了所有評論,Eloquent 並不會自動為每個子模型 `Comment` 補充父模型 `Post`,所以產生了「N + 1」查詢問題。

如果你希望 Eloquent 自動將父模型補充到其子模型上,你可以在定義 `hasMany` 關係時呼叫 `chaperone` 方法

<?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` 模型

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

一對多(反向)/ Belongs To

現在我們可以存取文章的所有評論,讓我們定義一個關係,允許評論存取其父文章。若要定義 `hasMany` 關係的反向關係,請在子模型上定義一個呼叫 `belongsTo` 方法的關係方法

<?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`「動態關係屬性」來檢索評論的父文章。

use App\Models\Comment;
 
$comment = Comment::find(1);
 
return $comment->post->title;

在上面的範例中,Eloquent 會嘗試尋找一個 `id` 符合 `Comment` 模型上 `post_id` 欄位的 `Post` 模型。

Eloquent 會檢視關係方法的名稱,並在其後加上底線 `_` 以及父模型的主鍵欄位名稱,來決定預設的外鍵名稱。因此,在這個範例中,Eloquent 會假設 `Post` 模型在 `comments` 表格上的外鍵是 `post_id`。

但是,如果你的關係的外鍵不符合這些慣例,你可以將自訂的外鍵名稱作為第二個參數傳遞給 `belongsTo` 方法

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

如果你的父模型沒有使用 `id` 作為其主鍵,或者你希望使用不同的欄位來尋找相關的模型,你可以將第三個參數傳遞給 `belongsTo` 方法,指定你的父表格的自訂鍵

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

預設模型

`belongsTo`、`hasOne`、`hasOneThrough` 和 `morphOne` 關係允許你定義一個預設模型,當給定的關係為 `null` 時,將返回該模型。這種模式通常被稱為空物件模式,可以幫助移除程式碼中的條件檢查。在下面的範例中,如果 `Post` 模型沒有關聯任何使用者,`user` 關係將返回一個空的 `App\Models\User` 模型

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

若要使用屬性填充預設模型,你可以將陣列或閉包傳遞給 `withDefault` 方法

/**
* 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 模型

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

但是,你可能會發現使用 `whereBelongsTo` 方法更方便,它會自動為給定的模型確定適當的關係和外鍵

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

你也可以將 集合 實例提供給 `whereBelongsTo` 方法。這樣做時,Laravel 將檢索屬於集合中任何父模型的模型

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

預設情況下,Laravel 將根據模型的類別名稱來決定與給定模型關聯的關係;但是,你可以透過將其作為 `whereBelongsTo` 方法的第二個參數來手動指定關係名稱

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

Has One of Many

有時一個模型可能有多個相關模型,但你希望輕鬆檢索關係中「最新」或「最舊」的相關模型。例如,一個 `User` 模型可能與多個 `Order` 模型相關聯,但你希望定義一種方便的方式來與使用者下的最新訂單互動。你可以使用 `hasOne` 關係類型結合 `ofMany` 方法來達成此目的

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

同樣地,你可以定義一個方法來檢索關係中「最舊」或第一個相關的模型

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

預設情況下,`latestOfMany` 和 `oldestOfMany` 方法會根據模型的主鍵檢索最新或最舊的相關模型,主鍵必須是可排序的。但是,有時你可能希望使用不同的排序條件從更大的關係中檢索單一模型。

例如,使用 `ofMany` 方法,你可以檢索使用者最昂貴的訂單。`ofMany` 方法接受可排序的欄位作為其第一個參數,以及在查詢相關模型時要套用的彙總函數(`min` 或 `max`)

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

因為 PostgreSQL 不支援對 UUID 欄位執行 `MAX` 函數,因此目前無法將 one-of-many 關係與 PostgreSQL UUID 欄位結合使用。

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

通常,當使用 `latestOfMany`、`oldestOfMany` 或 `ofMany` 方法檢索單一模型時,你已經為同一個模型定義了「has many」關係。為了方便起見,Laravel 允許你透過在關係上呼叫 `one` 方法,輕鬆地將此關係轉換為「has one」關係

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

進階的 Has One of Many 關係

可以建構更進階的「has one of many」關係。例如,`Product` 模型可能有多個相關聯的 `Price` 模型,這些模型即使在新價格發布後仍保留在系統中。此外,產品的新價格資料可能會提前發布,以在未來日期透過 `published_at` 欄位生效。

因此,總而言之,我們需要檢索發布日期不在未來的最新發布價格。此外,如果兩個價格具有相同的發布日期,我們將優先選擇具有最大 ID 的價格。若要完成此操作,我們必須將包含決定最新價格的可排序欄位的陣列傳遞給 `ofMany` 方法。此外,將提供閉包作為 `ofMany` 方法的第二個參數。此閉包將負責將其他發布日期限制新增至關係查詢

/**
* 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」關係定義與另一個模型的一對一關係。但是,此關係表示宣告的模型可以透過第三個模型與另一個模型的單一實例比對。

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

mechanics
id - integer
name - string
 
cars
id - integer
model - string
mechanic_id - integer
 
owners
id - integer
name - string
car_id - integer

現在我們已經檢查了關係的表格結構,讓我們在 `Mechanic` 模型上定義關係

<?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` 方法並提供這些關係的名稱,來流暢地定義「has-one-through」關係。例如,如果 `Mechanic` 模型具有 `cars` 關係,並且 `Car` 模型具有 `owner` 關係,你可以定義一個連接技工和車主的「has-one-through」關係,如下所示

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

鍵的慣例

在執行關係的查詢時,將使用典型的 Eloquent 外鍵慣例。如果你想要自訂關係的鍵,你可以將它們作為第三和第四個參數傳遞給 `hasOneThrough` 方法。第三個參數是中間模型上的外鍵名稱。第四個參數是最終模型上的外鍵名稱。第五個參數是本地鍵,而第六個參數是中間模型的本地鍵

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` 方法並提供這些關係的名稱,來流暢地定義「has-one-through」關係。這種方法提供了重複使用現有關係上已定義的鍵慣例的優勢

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

Has Many Through

「has-many-through」關係提供了一種透過中間關係存取遠距離關係的便捷方式。例如,假設我們正在建構像 Laravel Vapor 這樣的部署平台。一個 `Project` 模型可能會透過中間 `Environment` 模型存取多個 `Deployment` 模型。使用此範例,你可以輕鬆收集給定專案的所有部署。讓我們看看定義此關係所需的表格

projects
id - integer
name - string
 
environments
id - integer
project_id - integer
name - string
 
deployments
id - integer
environment_id - integer
commit_hash - string

現在我們已經檢查了關係的表格結構,讓我們在 `Project` 模型上定義關係

<?php
 
namespace App\Models;
 
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasManyThrough;
 
class Project extends Model
{
/**
* Get all of the deployments for the project.
*/
public function deployments(): HasManyThrough
{
return $this->hasManyThrough(Deployment::class, Environment::class);
}
}

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

或者,如果相關的關係已經在關係中涉及的所有模型上定義,你可以透過呼叫 `through` 方法並提供這些關係的名稱,來流暢地定義「has-many-through」關係。例如,如果 `Project` 模型具有 `environments` 關係,並且 `Environment` 模型具有 `deployments` 關係,你可以定義一個連接專案和部署的「has-many-through」關係,如下所示

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

雖然 `Deployment` 模型的表格不包含 `project_id` 欄位,但 `hasManyThrough` 關係提供透過 `$project->deployments` 存取專案部署的方法。若要檢索這些模型,Eloquent 會檢查中間 `Environment` 模型的表格上的 `project_id` 欄位。找到相關的環境 ID 後,它們會被用來查詢 `Deployment` 模型的表格。

鍵的慣例

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

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

或者,如先前所討論,如果相關的關係已經在關係中涉及的所有模型上定義,你可以透過呼叫 `through` 方法並提供這些關係的名稱,來流暢地定義「has-many-through」關係。這種方法提供了重複使用現有關係上已定義的鍵慣例的優勢

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

多對多關聯

多對多關係比 hasOnehasMany 關係稍微複雜。多對多關係的一個例子是,一個使用者擁有多個角色,而這些角色也由應用程式中的其他使用者共享。例如,一個使用者可能被指派「作者」和「編輯」的角色;然而,這些角色也可能被指派給其他使用者。因此,一個使用者擁有多個角色,而一個角色也擁有多個使用者。

資料表結構

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

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

users
id - integer
name - string
 
roles
id - integer
name - string
 
role_user
user_id - integer
role_id - integer

模型結構

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

<?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 動態關係屬性存取使用者的角色

use App\Models\User;
 
$user = User::find(1);
 
foreach ($user->roles as $role) {
// ...
}

由於所有關係也充當查詢建構器,您可以使用調用 roles 方法並繼續將條件鏈接到查詢來向關係查詢添加更多約束

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

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

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

除了自訂中間表的名稱之外,您還可以通過將額外參數傳遞給 belongsToMany 方法來自訂表格上索引鍵的欄位名稱。第三個參數是您在其中定義關係的模型的外來索引鍵名稱,而第四個參數是您要加入的模型的外來索引鍵名稱

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

定義關聯的反向關係

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

<?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);
}
}

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

檢索中介資料表欄位

正如您已經了解到的,使用多對多關係需要中間表格的存在。Eloquent 提供了一些非常有用的方法來與此表格互動。例如,假設我們的 User 模型與多個 Role 模型相關。在存取此關係之後,我們可以使用模型上的 pivot 屬性存取中間表格

use App\Models\User;
 
$user = User::find(1);
 
foreach ($user->roles as $role) {
echo $role->pivot->created_at;
}

請注意,我們檢索的每個 Role 模型都會自動指派一個 pivot 屬性。此屬性包含表示中間表格的模型。

預設情況下,只有模型索引鍵才會出現在 pivot 模型上。如果您的中間表格包含額外的屬性,您必須在定義關係時指定它們

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

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

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

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

自訂 pivot 屬性名稱

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

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

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

一旦指定了自訂中間表格屬性,您可以使用自訂名稱存取中間表格數據

$users = User::with('podcasts')->get();
 
foreach ($users->flatMap->podcasts as $podcast) {
echo $podcast->subscription->created_at;
}

透過中介資料表欄位篩選查詢

您也可以使用定義關係時的 wherePivotwherePivotInwherePivotNotInwherePivotBetweenwherePivotNotBetweenwherePivotNullwherePivotNotNull 方法來篩選 belongsToMany 關係查詢返回的結果

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

透過中介資料表欄位排序查詢

您可以使用 orderByPivot 方法對 belongsToMany 關係查詢返回的結果進行排序。在以下範例中,我們將檢索使用者的所有最新徽章

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

定義自訂中介資料表模型

如果您想定義一個自訂模型來表示多對多關係的中間表格,您可以在定義關係時呼叫 using 方法。自訂樞紐模型讓您有機會在樞紐模型上定義其他行為,例如方法和類型轉換。

自訂多對多樞紐模型應擴展 Illuminate\Database\Eloquent\Relations\Pivot 類別,而自訂多型多對多樞紐模型應擴展 Illuminate\Database\Eloquent\Relations\MorphPivot 類別。例如,我們可以定義一個使用自訂 RoleUser 樞紐模型的 Role 模型

<?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
 
namespace App\Models;
 
use Illuminate\Database\Eloquent\Relations\Pivot;
 
class RoleUser extends Pivot
{
// ...
}
exclamation

樞紐模型不能使用 SoftDeletes 特性。如果您需要軟刪除樞紐記錄,請考慮將您的樞紐模型轉換為實際的 Eloquent 模型。

自訂樞紐模型和遞增 ID

如果您定義了使用自訂樞紐模型的多對多關係,並且該樞紐模型具有自動遞增的主索引鍵,您應確保您的自訂樞紐模型類別定義一個設定為 trueincrementing 屬性。

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

多態關聯

多型關係允許子模型使用單一關聯屬於多種類型的模型。例如,假設您正在建立一個允許使用者分享部落格文章和影片的應用程式。在這樣的應用程式中,Comment 模型可能同時屬於 PostVideo 模型。

一對一 (多型)

資料表結構

一對一多型關係與典型的一對一關係類似;然而,子模型可以使用單一關聯屬於多種類型的模型。例如,部落格 PostUser 可能與 Image 模型共享多型關係。使用一對一多型關係允許您擁有一個獨特影像的單一表格,該表格可以與文章和使用者相關聯。首先,讓我們檢查一下資料表結構

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 欄位將包含父模型的類別名稱。imageable_type 欄位由 Eloquent 用於在存取 imageable 關係時確定要返回的父模型「類型」。在這種情況下,該欄位將包含 App\Models\PostApp\Models\User

模型結構

接下來,讓我們檢查建立此關係所需的模型定義

<?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 動態關係屬性

use App\Models\Post;
 
$post = Post::find(1);
 
$image = $post->image;

您可以通過存取執行 morphTo 呼叫的方法名稱來檢索多型模型的父項。在這種情況下,它是 Image 模型上的 imageable 方法。因此,我們將作為動態關係屬性存取該方法

use App\Models\Image;
 
$image = Image::find(1);
 
$imageable = $image->imageable;

Image 模型上的 imageable 關係將返回 PostUser 實例,具體取決於哪個類型的模型擁有該影像。

鍵的慣例

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

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

一對多 (多型)

資料表結構

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

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
 
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 動態屬性

use App\Models\Post;
 
$post = Post::find(1);
 
foreach ($post->comments as $comment) {
// ...
}

您也可以通過存取執行 morphTo 呼叫的方法名稱來檢索多型子模型的父項。在這種情況下,它是 Comment 模型上的 commentable 方法。因此,我們將作為動態關係屬性存取該方法,以便存取評論的父模型

use App\Models\Comment;
 
$comment = Comment::find(1);
 
$commentable = $comment->commentable;

Comment 模型上的 commentable 關係將返回 PostVideo 實例,具體取決於哪個類型的模型是評論的父項。

自動為子模型補充父模型

即使使用 Eloquent 預先載入 (eager loading),如果你在迴圈遍歷子模型時嘗試存取父模型,仍可能發生「N + 1」查詢問題

$posts = Post::with('comments')->get();
 
foreach ($posts as $post) {
foreach ($post->comments as $comment) {
echo $comment->commentable->title;
}
}

在上面的範例中,因為即使 `Post` 模型已預先載入了所有評論,Eloquent 並不會自動為每個子模型 `Comment` 補充父模型 `Post`,所以產生了「N + 1」查詢問題。

如果您希望 Eloquent 自動將父模型水合到其子模型上,您可以在定義 morphMany 關係時調用 chaperone 方法

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

或者,如果你想在執行時選擇啟用自動父模型補充,你可以在預先載入關係時呼叫 `chaperone` 模型

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

多個之一 (多型)

有時,一個模型可能有多個相關模型,但您希望輕鬆檢索關係的「最新」或「最舊」的相關模型。例如,User 模型可能與多個 Image 模型相關,但您希望定義一種方便的方式來與使用者上傳的最新影像互動。您可以使用 morphOne 關係類型與 ofMany 方法結合來實現此目的

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

同樣地,你可以定義一個方法來檢索關係中「最舊」或第一個相關的模型

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

預設情況下,`latestOfMany` 和 `oldestOfMany` 方法會根據模型的主鍵檢索最新或最舊的相關模型,主鍵必須是可排序的。但是,有時你可能希望使用不同的排序條件從更大的關係中檢索單一模型。

例如,使用 ofMany 方法,您可以檢索使用者最「喜歡」的影像。ofMany 方法接受可排序的欄位作為其第一個參數,並接受在查詢相關模型時要應用的聚合函數 (minmax)

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

可以建構更進階的「多個之一」關係。如需更多資訊,請查閱 多個之一的進階文件

多對多 (多型)

資料表結構

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

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
lightbulb

在深入探討多對多態關聯之前,您可能可以先閱讀關於典型的多對多關聯的文件。

模型結構

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

morphToMany 方法接受相關模型的名稱以及「關係名稱」。根據我們指定的中介資料表名稱及其包含的鍵,我們會將關係稱為「taggable」。

<?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
 
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 動態關係屬性。

use App\Models\Post;
 
$post = Post::find(1);
 
foreach ($post->tags as $tag) {
// ...
}

您可以透過存取執行 morphedByMany 呼叫的方法名稱,從多態子模型檢索多態關係的父模型。在本例中,它是 Tag 模型上的 postsvideos 方法。

use App\Models\Tag;
 
$tag = Tag::find(1);
 
foreach ($tag->posts as $post) {
// ...
}
 
foreach ($tag->videos as $video) {
// ...
}

自訂多態類型

預設情況下,Laravel 將使用完整限定的類別名稱來儲存相關模型的「類型」。例如,以上述一對多關係範例為例,其中 Comment 模型可能屬於 PostVideo 模型,預設的 commentable_type 將分別為 App\Models\PostApp\Models\Video。但是,您可能希望將這些值與應用程式的內部結構分離。

例如,我們可以使用簡單的字串(例如 postvideo)來取代使用模型名稱作為「類型」。這樣做的話,即使模型被重新命名,資料庫中多態「類型」欄位的值仍然有效。

use Illuminate\Database\Eloquent\Relations\Relation;
 
Relation::enforceMorphMap([
'post' => 'App\Models\Post',
'video' => 'App\Models\Video',
]);

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

您可以使用模型的 getMorphClass 方法在執行時判斷給定模型的 morph 別名。反之,您可以使用 Relation::getMorphedModel 方法判斷與 morph 別名相關聯的完整限定的類別名稱。

use Illuminate\Database\Eloquent\Relations\Relation;
 
$alias = $post->getMorphClass();
 
$class = Relation::getMorphedModel($alias);
exclamation

當將「morph map」新增到您現有的應用程式時,資料庫中每個仍然包含完整限定類別的 morphable *_type 欄位值都需要轉換為其「map」名稱。

動態關聯

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

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

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

在定義動態關係時,請始終為 Eloquent 關係方法提供明確的鍵名稱引數。

查詢關聯

由於所有 Eloquent 關係都是透過方法定義的,您可以呼叫這些方法來取得關係的實例,而無需實際執行查詢來載入相關模型。此外,所有類型的 Eloquent 關係也可以充當查詢建構器,讓您可以在最終對資料庫執行 SQL 查詢之前,繼續將約束條件鏈接到關係查詢。

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

<?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 關係,並像這樣將其他約束條件新增到關係中。

use App\Models\User;
 
$user = User::find(1);
 
$user->posts()->where('active', 1)->get();

您可以在關係上使用任何 Laravel 查詢建構器的方法,因此請務必瀏覽查詢建構器的文件,以瞭解所有可用的方法。

在關係之後鏈接 orWhere 子句

如上例所示,您可以在查詢關係時自由地將其他約束條件新增到關係中。但是,當將 orWhere 子句鏈接到關係時,請務必謹慎,因為 orWhere 子句將在邏輯上與關係約束條件分組在同一層級。

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

上面的範例將產生以下 SQL。正如您所看到的,or 子句指示查詢傳回任何投票數大於 100 的文章。查詢不再限定於特定使用者。

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

在大多數情況下,您應該使用邏輯分組將條件檢查分組在括號之間。

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

上面的範例將產生以下 SQL。請注意,邏輯分組已正確地將約束條件分組,並且查詢仍然限定於特定使用者。

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

關聯方法 vs. 動態屬性

如果您不需要將其他約束條件新增到 Eloquent 關係查詢,您可以像存取屬性一樣存取關係。例如,繼續使用我們的 UserPost 範例模型,我們可以像這樣存取使用者所有的文章。

use App\Models\User;
 
$user = User::find(1);
 
foreach ($user->posts as $post) {
// ...
}

動態關係屬性執行「延遲載入」,這表示它們只會在您實際存取時才載入它們的關係資料。因此,開發人員經常使用預先載入來預先載入他們知道在載入模型之後將會存取的關係。預先載入可以大幅減少為了載入模型的關係而必須執行的 SQL 查詢。

查詢關聯是否存在

在檢索模型記錄時,您可能希望根據關係的存在來限制您的結果。例如,假設您想要檢索至少有一個註解的所有部落格文章。為此,您可以將關係的名稱傳遞給 hasorHas 方法。

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

您也可以指定一個運算符和計數值,以進一步自訂查詢。

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

可以使用「點」表示法來建構巢狀的 has 陳述式。例如,您可以檢索所有至少有一個註解(且該註解至少有一張圖片)的文章。

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

如果您需要更大的控制力,您可以使用 whereHasorWhereHas 方法,在您的 has 查詢中定義其他查詢約束條件,例如檢查註解的內容。

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();
exclamation

Eloquent 目前不支援跨資料庫查詢關係是否存在。關係必須存在於同一個資料庫中。

內嵌關係存在查詢

如果您想要使用單一、簡單的 where 條件連結到關係查詢來查詢關係的存在,您可能會發現使用 whereRelationorWhereRelationwhereMorphRelationorWhereMorphRelation 方法更方便。例如,我們可以查詢所有有未核准註解的文章。

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

當然,就像呼叫查詢建構器的 where 方法一樣,您也可以指定一個運算符。

$posts = Post::whereRelation(
'comments', 'created_at', '>=', now()->subHour()
)->get();

查詢關聯是否不存在

在檢索模型記錄時,您可能希望根據關係的不存在來限制您的結果。例如,假設您想要檢索沒有任何註解的所有部落格文章。為此,您可以將關係的名稱傳遞給 doesntHaveorDoesntHave 方法。

use App\Models\Post;
 
$posts = Post::doesntHave('comments')->get();

如果您需要更大的控制力,您可以使用 whereDoesntHaveorWhereDoesntHave 方法,將其他查詢約束條件新增到您的 doesntHave 查詢中,例如檢查註解的內容。

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

您可以使用「點」表示法對巢狀關係執行查詢。例如,以下查詢將檢索沒有註解的所有文章;但是,來自未被禁止的作者的註解的文章將包含在結果中。

use Illuminate\Database\Eloquent\Builder;
 
$posts = Post::whereDoesntHave('comments.author', function (Builder $query) {
$query->where('banned', 0);
})->get();

查詢 Morph To 關聯

要查詢「morph to」關係的存在,您可以使用 whereHasMorphwhereDoesntHaveMorph 方法。這些方法接受關係的名稱作為它們的第一個引數。接下來,這些方法接受您希望包含在查詢中的相關模型的名稱。最後,您可以提供一個自訂關係查詢的閉包。

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

有時您可能需要根據相關多態模型的「類型」來新增查詢約束條件。傳遞給 whereHasMorph 方法的閉包可以接收一個 $type 值作為其第二個引數。此引數可讓您檢查正在建構的查詢「類型」。

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 方法來完成此操作,這些方法將自動判斷給定模型的適當 morph 類型對應。這些方法接受 morphTo 關係的名稱作為其第一個引數,以及相關的父模型作為其第二個引數。

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

您可以提供 * 作為萬用字元值,而不是傳遞可能的 Polymorphic 模型陣列。這將指示 Laravel 從資料庫中檢索所有可能的多態類型。Laravel 將執行額外的查詢以執行此操作。

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

有時您可能想要計算給定關係的相關模型數量,而無需實際載入模型。為此,您可以使用 withCount 方法。withCount 方法將在產生的模型上放置一個 {relation}_count 屬性。

use App\Models\Post;
 
$posts = Post::withCount('comments')->get();
 
foreach ($posts as $post) {
echo $post->comments_count;
}

透過將陣列傳遞給 withCount 方法,您可以新增多個關係的「計數」,並將其他約束條件新增到查詢中。

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;

您也可以為關係計數結果設定別名,允許在同一個關係上進行多個計數。

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 方法,您可以在父模型已經被檢索之後載入關係計數。

$book = Book::first();
 
$book->loadCount('genres');

如果您需要在計數查詢中設定其他查詢約束條件,您可以傳遞一個以您想要計數的關係為鍵的陣列。陣列值應該是接收查詢建構器實例的閉包。

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

關係計數和自訂 Select 陳述式

如果您要將 withCountselect 陳述式結合使用,請確保在 select 方法之後呼叫 withCount

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

其他彙總函式

除了 withCount 方法之外,Eloquent 還提供 withMinwithMaxwithAvgwithSumwithExists 方法。這些方法會在產生的模型上放置一個 {relation}_{function}_{column} 屬性。

use App\Models\Post;
 
$posts = Post::withSum('comments', 'votes')->get();
 
foreach ($posts as $post) {
echo $post->comments_sum_votes;
}

如果您想要使用其他名稱存取彙總函數的結果,您可以指定自己的別名。

$posts = Post::withSum('comments as total_comments', 'votes')->get();
 
foreach ($posts as $post) {
echo $post->total_comments;
}

loadCount 方法一樣,這些方法的延遲版本也可用。這些額外的彙總運算可以在已經檢索的 Eloquent 模型上執行。

$post = Post::first();
 
$post->loadSum('comments', 'votes');

如果您將這些彙總方法與 select 語句結合使用,請確保在 select 方法之後呼叫彙總方法。

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

如果您想要預先載入「多態關聯」(morph to) 關係,以及該關係可能傳回的各種實體的相關模型計數,您可以結合使用 with 方法和「多態關聯」的 morphWithCount 方法。

在這個範例中,假設 PhotoPost 模型可以建立 ActivityFeed 模型。我們假設 ActivityFeed 模型定義了一個名為 parentable 的「多態關聯」,它允許我們檢索給定 ActivityFeed 實例的父 PhotoPost 模型。此外,假設 Photo 模型「擁有多個」 Tag 模型,而 Post 模型「擁有多個」 Comment 模型。

現在,假設我們想要檢索 ActivityFeed 實例,並為每個 ActivityFeed 實例預先載入 parentable 父模型。此外,我們想要檢索與每個父相片相關聯的標籤數量,以及與每個父文章相關聯的評論數量。

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

延遲計數載入

假設我們已經檢索了一組 ActivityFeed 模型,現在我們想要載入與活動 Feed 相關聯的各種 parentable 模型的巢狀關係計數。您可以使用 loadMorphCount 方法來完成此操作。

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

預先載入

當將 Eloquent 關係作為屬性存取時,相關的模型會被「延遲載入」。這表示關係資料實際上不會載入,直到您第一次存取該屬性。但是,Eloquent 可以在您查詢父模型時「預先載入」關係。預先載入可以減輕「N + 1」查詢問題。為了說明 N + 1 查詢問題,請考慮一個「屬於」 Author 模型的 Book 模型。

<?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);
}
}

現在,讓我們檢索所有書籍及其作者。

use App\Models\Book;
 
$books = Book::all();
 
foreach ($books as $book) {
echo $book->author->name;
}

這個迴圈會執行一個查詢以檢索資料庫表格中的所有書籍,然後為每本書執行另一個查詢,以檢索該書籍的作者。因此,如果我們有 25 本書,上面的程式碼將執行 26 個查詢:一個用於原始書籍,以及 25 個額外查詢來檢索每本書的作者。

幸運的是,我們可以使用預先載入將此操作減少為僅兩個查詢。在建構查詢時,您可以使用 with 方法指定應該預先載入哪些關係。

$books = Book::with('author')->get();
 
foreach ($books as $book) {
echo $book->author->name;
}

對於此操作,只會執行兩個查詢 - 一個查詢以檢索所有書籍,另一個查詢以檢索所有書籍的所有作者。

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

預先載入多個關係

有時您可能需要預先載入幾個不同的關係。為此,只需將關係陣列傳遞給 with 方法。

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

巢狀預先載入

若要預先載入關係的關係,您可以使用「點」語法。例如,讓我們預先載入所有書籍的作者以及所有作者的個人聯絡人。

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

或者,您可以透過將巢狀陣列提供給 with 方法來指定巢狀預先載入的關係,這在預先載入多個巢狀關係時會很方便。

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

巢狀預先載入 morphTo 關係

如果您想要預先載入 morphTo 關係,以及該關係可能傳回的各種實體的巢狀關係,您可以結合使用 with 方法和 morphTo 關係的 morphWith 方法。為了說明此方法,讓我們考慮以下模型:

<?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 模型及其各自的巢狀關係。

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 允許您指定想要檢索的關係的欄位。

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

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

預設預先載入

有時您可能希望在檢索模型時始終載入某些關係。若要完成此操作,您可以在模型上定義 $with 屬性。

<?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 方法。

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

如果您想要覆寫單一查詢的 $with 屬性中的所有項目,您可以使用 withOnly 方法。

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

限制預先載入

有時您可能希望預先載入關係,但也為預先載入查詢指定其他查詢條件。您可以透過將關係陣列傳遞給 with 方法來完成此操作,其中陣列鍵是關係名稱,而陣列值是將其他約束新增至預先載入查詢的閉包。

use App\Models\User;
use Illuminate\Contracts\Database\Eloquent\Builder;
 
$users = User::with(['posts' => function (Builder $query) {
$query->where('title', 'like', '%code%');
}])->get();

在此範例中,Eloquent 只會預先載入文章的 title 欄位包含單字 code 的文章。您可以呼叫其他查詢建構器方法來進一步自訂預先載入操作。

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

限制預先載入 morphTo 關係

如果您正在預先載入 morphTo 關係,Eloquent 將執行多個查詢來擷取每種類型的相關模型。您可以使用 MorphTo 關係的 constrain 方法,將其他約束新增至每個查詢。

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 方法來完成此操作。

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

延遲預先載入

有時您可能需要在父模型檢索後預先載入關係。例如,如果您需要動態決定是否載入相關模型,這可能會很有用。

use App\Models\Book;
 
$books = Book::all();
 
if ($someCondition) {
$books->load('author', 'publisher');
}

如果您需要在預先載入查詢上設定其他查詢約束,您可以傳遞以您想要載入的關係為鍵的陣列。陣列值應為接收查詢實例的閉包實例。

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

若要僅在尚未載入關係時載入關係,請使用 loadMissing 方法。

$book->loadMissing('author');

巢狀延遲預先載入和 morphTo

如果您想要預先載入 morphTo 關係,以及該關係可能傳回的各種實體的巢狀關係,您可以使用 loadMorph 方法。

此方法接受 morphTo 關係的名稱作為其第一個引數,以及模型/關係配對的陣列作為其第二個引數。為了說明此方法,讓我們考慮以下模型:

<?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 模型及其各自的巢狀關係。

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

防止延遲載入

如先前所述,預先載入關係通常可以為您的應用程式提供顯著的效能優勢。因此,如果您願意,您可以指示 Laravel 永遠阻止延遲載入關係。若要完成此操作,您可以叫用基礎 Eloquent 模型類別提供的 preventLazyLoading 方法。通常,您應該在應用程式的 AppServiceProvider 類別的 boot 方法中呼叫此方法。

preventLazyLoading 方法接受一個可選的布林引數,指示是否應阻止延遲載入。例如,您可能希望僅在非生產環境中停用延遲載入,以便即使在生產程式碼中意外存在延遲載入的關係,您的生產環境仍能正常運作。

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

在阻止延遲載入之後,當您的應用程式嘗試延遲載入任何 Eloquent 關係時,Eloquent 將擲回 Illuminate\Database\LazyLoadingViolationException 例外狀況。

您可以使用 handleLazyLoadingViolationsUsing 方法自訂延遲載入違規的行為。例如,使用此方法,您可以指示延遲載入違規僅被記錄,而不是使用例外狀況中斷應用程式的執行。

Model::handleLazyLoadingViolationUsing(function (Model $model, string $relation) {
$class = $model::class;
 
info("Attempted to lazy load [{$relation}] on model [{$class}].");
});

save 方法

Eloquent 提供了方便的方法來將新模型新增至關係。例如,也許您需要將新的評論新增至文章。您可以透過使用關係的 save 方法插入評論,而不是手動在 Comment 模型上設定 post_id 屬性。

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 方法。

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

savesaveMany 方法會保存給定的模型實例,但不會將新保存的模型新增至已載入到父模型上的任何記憶體內關係。如果您計劃在使用 savesaveMany 方法後存取關係,您可能希望使用 refresh 方法重新載入模型及其關係。

$post->comments()->save($comment);
 
$post->refresh();
 
// All comments, including the newly saved comment...
$post->comments;

遞迴儲存模型和關係

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

$post = Post::find(1);
 
$post->comments[0]->message = 'Message';
$post->comments[0]->author->name = 'Author Name';
 
$post->push();

pushQuietly 方法可用於儲存模型及其相關聯的關係,而不會引發任何事件。

$post->pushQuietly();

create 方法

除了 savesaveMany 方法之外,您也可以使用 create 方法,該方法接受屬性陣列、建立模型並將其插入資料庫。savecreate 之間的差異在於 save 接受完整的 Eloquent 模型實例,而 create 接受純 PHP array。新建立的模型將由 create 方法傳回。

use App\Models\Post;
 
$post = Post::find(1);
 
$comment = $post->comments()->create([
'message' => 'A new comment.',
]);

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

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

createQuietlycreateManyQuietly 方法可用於建立模型,而不會傳送任何事件。

$user = User::find(1);
 
$user->posts()->createQuietly([
'title' => 'Post title.',
]);
 
$user->posts()->createManyQuietly([
['title' => 'First post.'],
['title' => 'Second post.'],
]);

您也可以使用 findOrNewfirstOrNewfirstOrCreateupdateOrCreate 方法來在關係上建立和更新模型

lightbulb

在使用 create 方法之前,請務必檢閱大量賦值文件。

Belongs To 關聯

如果您想要將子模型指派給新的父模型,您可以使用 associate 方法。在此範例中,User 模型定義了與 Account 模型的 belongsTo 關係。此 associate 方法將在子模型上設定外鍵。

use App\Models\Account;
 
$account = Account::find(10);
 
$user->account()->associate($account);
 
$user->save();

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

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

多對多關聯

附加/分離

Eloquent 也提供了更方便處理多對多關係的方法。舉例來說,假設一個使用者可以擁有多個角色,而一個角色也可以擁有多個使用者。您可以使用 attach 方法,透過在關係的中介資料表中插入一筆記錄,將一個角色附加到一個使用者上。

use App\Models\User;
 
$user = User::find(1);
 
$user->roles()->attach($roleId);

當將關係附加到模型時,您也可以傳遞一個額外資料的陣列,以便插入到中介資料表中。

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

有時可能需要從使用者中移除一個角色。要移除多對多關係的記錄,請使用 detach 方法。detach 方法將從中介資料表中刪除適當的記錄;但是,兩個模型都將保留在資料庫中。

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

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

$user = User::find(1);
 
$user->roles()->detach([1, 2, 3]);
 
$user->roles()->attach([
1 => ['expires' => $expires],
2 => ['expires' => $expires],
]);

同步關聯

您也可以使用 sync 方法來建立多對多關聯。sync 方法接受一個 ID 的陣列,以便放置在中介資料表中。任何不在給定陣列中的 ID 都將從中介資料表中移除。因此,在此操作完成後,中介資料表中將僅存在給定陣列中的 ID。

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

您也可以透過 ID 傳遞額外的中介資料表值。

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

如果您想為每個同步的模型 ID 插入相同的中介資料表值,您可以使用 syncWithPivotValues 方法。

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

如果您不想分離給定陣列中缺少之現有 ID,您可以使用 syncWithoutDetaching 方法。

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

切換關聯

多對多關係也提供了一個 toggle 方法,用於「切換」給定相關模型 ID 的附加狀態。如果給定的 ID 目前已附加,則將會被分離。同樣地,如果它目前已分離,則將會被附加。

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

您也可以透過 ID 傳遞額外的中介資料表值。

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

更新中介資料表上的記錄

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

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

更新父時間戳記

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

舉例來說,當 Comment 模型更新時,您可能會想要自動「觸發」擁有者 Postupdated_at 時間戳記,使其設定為目前的日期和時間。若要達成此目的,您可以在子模型中新增一個 touches 屬性,其中包含在子模型更新時應該更新其 updated_at 時間戳記的關係名稱。

<?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);
}
}
exclamation

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