Eloquent: Relationships
簡介 (Introduction)
資料表通常彼此相關。例如,部落格文章可能有許多留言,或訂單可能與下訂單的使用者相關。Eloquent 使得管理和使用這些關聯變得簡單,並支援各種常見的關聯類型:
定義關聯 (Defining Relationships)
Eloquent 關聯定義為 Eloquent 模型類別上的方法。由於關聯也充當強大的 查詢建構器,將關聯定義為方法能提供強大的方法鏈接與查詢功能。例如,我們可在此 posts 關聯上串接額外的查詢條件:
$user->posts()->where('active', 1)->get();
但在深入使用關聯前,讓我們先學習 Eloquent 支援的各種關聯類型的定義方式。
一對一 / 擁有一個 (One to One / Has One)
一對一關聯是最基本的資料庫關聯類型。例如,User 模型可能關聯到一個 Phone 模型。要定義此關聯,我們需在 User 模型上放置 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');
定義關聯的反向 (Defining the Inverse of the Relationship)
我們已能從 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 會嘗試找到 id 與 Phone 模型中 user_id 欄位相符的 User 模型。
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');
}
一對多 / 擁有多個 (One to Many / 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');
自動為子模型水合父模型 (Automatically Hydrating Parent Models on Children)
即使使用 Eloquent 動態載入,如果在迴圈子模型時試著從子模型存取父模型,也會產生「N + 1」查詢問題:
$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
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();
一對多(反向)/ 屬於 (One to Many (Inverse) / Belongs To)
現在我們已能存取文章的所有評論,讓我們在 Comment 模型上定義一個關聯,以使評論能夠存取其父文章。要定義 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');
}
預設模型 (Default Models)
belongsTo、hasOne、hasOneThrough 和 morphOne 關聯允許你定義一個預設模型,如果給定的關聯為 null,則會回傳該模型。這種模式通常被稱為 Null Object pattern,可以幫助移除程式碼中的條件檢查。在以下範例中,如果沒有使用者附加到 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';
});
}
查詢屬於關聯 (Querying Belongs To Relationships)
從「屬於」關聯的子模型查詢時,可以手動建構 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');
}
[!WARNING] 由於 PostgreSQL 不支援對 UUID 欄位執行
MAX函數,目前無法將一對多之其一關聯與 PostgreSQL UUID 欄位結合使用。
將「多」關聯轉換為「一」關聯 (Converting "Many" Relationships to Has One (Converting Many Relationships To Has One Relationships)
Relationships)
通常,當使用 latestOfMany、oldestOfMany 或 ofMany 方法檢索單個模型時,你已經為同一模型定義了「一對多」關聯。為了方便起見,Laravel 允許你通過在關聯上呼叫 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');
}
你也可以使用 one 方法將 HasManyThrough 關聯轉換為 HasOneThrough 關聯:
public function latestDeployment(): HasOneThrough
{
return $this->deployments()->one()->latestOfMany();
}
進階的一對多之其一關聯 (Advanced Has One of Many Relationships)
可以建構更進階的「一對多之其一」關聯。例如,一個 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)
「遠層一對一」關聯定義了與另一個模型的一對一關聯。但是,這種關聯表示宣告模型可以通過第三個模型與另一個模型的一個實例相匹配。
例如,在汽車維修店應用程式中,每個 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 方法並提供這些關聯的名稱來流暢地定義「遠層一對一」關聯。例如,如果 Mechanic 模型有一個 cars 關聯,而 Car 模型有一個 owner 關聯,你可以像這樣定義連接技師和車主的「遠層一對一」關聯:
// String based syntax...
return $this->through('cars')->has('owner');
// Dynamic syntax...
return $this->throughCars()->hasOwner();
鍵名慣例 (Key Conventions)
執行關聯查詢時將使用典型的 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 方法並提供這些關聯的名稱來流暢地定義「遠層一對一」關聯。這種方法提供了重用現有關聯上已定義的鍵名慣例的優點:
// String based syntax...
return $this->through('cars')->has('owner');
// Dynamic syntax...
return $this->throughCars()->hasOwner();
遠層一對多 (Has Many Through)
「遠層一對多」關聯提供了一種通過中間關聯存取遠端關聯的便捷方法。例如,讓我們假設我們正在建立一個像 Laravel Cloud 這樣的部署平台。一個 Application 模型可能通過中間的 Environment 模型存取許多 Deployment 模型。使用此範例,你可以輕鬆收集給定應用程式的所有部署。讓我們看看定義此關聯所需的資料表:
applications
id - integer
name - string
environments
id - integer
application_id - integer
name - string
deployments
id - integer
environment_id - integer
commit_hash - string
現在我們已經檢查了關聯的資料表結構,讓我們在 Application 模型上定義關聯:
<?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 關聯,你可以像這樣定義連接應用程式和部署的「遠層一對多」關聯:
// 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 方法。第三個參數是中間模型上的外鍵名稱。第四個參數是最終模型上的外鍵名稱。第五個參數是本地鍵,而第六個參數是中間模型的本地鍵:
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 方法並提供這些關聯的名稱來流暢地定義「遠層一對多」關聯。這種方法提供了重用現有關聯上已定義的鍵名慣例的優點:
// String based syntax...
return $this->through('environments')->has('deployments');
// Dynamic syntax...
return $this->throughEnvironments()->hasDeployments();
範圍關聯 (Scoped Relationships)
在模型中新增額外的方法來約束關聯是很常見的。例如,你可以在 User 模型中新增一個 featuredPosts 方法,該方法通過額外的 where 約束來限制更廣泛的 posts 關聯:
<?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 方法:
/**
* Get the user's featured posts.
*/
public function featuredPosts(): HasMany
{
return $this->posts()->withAttributes(['featured' => true]);
}
withAttributes 方法將使用給定的屬性向查詢新增 where 條件,並且還會將給定的屬性新增到通過關聯方法建立的任何模型中:
$post = $user->featuredPosts()->create(['title' => 'Featured Post']);
$post->featured; // true
要指示 withAttributes 方法不要向查詢新增 where 條件,你可以將 asConditions 參數設定為 false:
return $this->posts()->withAttributes(['featured' => true], asConditions: false);
多對多關聯 (Many to Many Relationships)
多對多關聯比 hasOne 和 hasMany 關聯稍微複雜一些。多對多關聯的一個例子是,一個使用者有多個角色,而這些角色也由應用程式中的其他使用者共享。例如,一個使用者可能被分配了「作者」和「編輯」的角色;但是,這些角色也可能分配給其他使用者。因此,一個使用者有多個角色,一個角色有多個使用者。
資料表結構 (Table Structure)
要定義此關聯,需要三個資料庫資料表:users、roles 和 role_user。role_user 資料表是根據相關模型名稱的字母順序衍生的,並包含 user_id 和 role_id 欄位。此資料表用作連接使用者和角色的中間資料表。
記住,由於一個角色可以屬於多個使用者,我們不能簡單地在 roles 資料表上放置一個 user_id 欄位。這將意味著一個角色只能屬於單個使用者。為了支援將角色分配給多個使用者,需要 role_user 資料表。我們可以像這樣總結關聯的資料表結構:
users
id - integer
name - string
roles
id - integer
name - string
role_user
user_id - integer
role_id - integer
模型結構 (Model Structure)
多對多關聯是通過編寫一個回傳 belongsToMany 方法結果的方法來定義的。belongsToMany 方法由 Illuminate\Database\Eloquent\Model 基底類別提供,該類別由你的應用程式的所有 Eloquent 模型使用。例如,讓我們在 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');
定義關聯的反向 (Defining the Inverse of the Relationship)
要定義多對多關聯的「反向」,你應該在相關模型上定義一個方法,該方法也回傳 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);
}
}
如你所見,除了參照 App\Models\User 模型外,該關聯的定義與其 User 模型對應部分完全相同。由於我們重用了 belongsToMany 方法,因此在定義多對多關聯的「反向」時,所有常用的資料表和鍵自訂選項均可用。
檢索中間資料表欄位 (Retrieving Intermediate Table Columns)
如你所知,使用多對多關聯需要中間資料表的存在。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_at 和 updated_at 時間戳記,請在定義關聯時呼叫 withTimestamps 方法:
return $this->belongsToMany(Role::class)->withTimestamps();
[!WARNING] 使用 Eloquent 自動維護時間戳記的中間資料表必須同時具有
created_at和updated_at時間戳記欄位。
自訂 pivot 屬性名稱 (Customizing the pivot Attribute Name)
如前所述,可以通過 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;
}
透過中間資料表欄位過濾查詢 (Filtering Queries via Intermediate Table (Filtering Queries via Intermediate Table Columns)
Columns)
你還可以在定義關聯時使用 wherePivot、wherePivotIn、wherePivotNotIn、wherePivotBetween、wherePivotNotBetween、wherePivotNull 和 wherePivotNotNull 方法來過濾 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');
wherePivot 向查詢新增 where 子句約束,但在通過定義的關聯建立新模型時不會新增指定的值。如果你需要查詢和建立具有特定 pivot 值的關聯,你可以使用 withPivotValue 方法:
return $this->belongsToMany(Role::class)
->withPivotValue('approved', 1);
透過中間資料表欄位排序查詢 (Ordering Queries via Intermediate Table Columns)
你可以使用 orderByPivot 方法對 belongsToMany 關聯查詢回傳的結果進行排序。在以下範例中,我們將檢索使用者的所有最新徽章:
return $this->belongsToMany(Badge::class)
->where('rank', 'gold')
->orderByPivot('created_at', 'desc');
定義自訂中間資料表模型 (Defining Custom Intermediate Table Models)
如果你想定義一個自訂模型來表示多對多關聯的中間資料表,你可以在定義關聯時呼叫 using 方法。自訂 pivot 模型讓你有機會在 pivot 模型上定義額外的行為,例如方法和轉換。
自訂多對多 pivot 模型應繼承 Illuminate\Database\Eloquent\Relations\Pivot 類別,而自訂多型多對多 pivot 模型應繼承 Illuminate\Database\Eloquent\Relations\MorphPivot 類別。例如,我們可以定義一個使用自訂 RoleUser pivot 模型的 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
{
// ...
}
[!WARNING] Pivot models may not use the
SoftDeletestrait. If you need to soft delete pivot records consider converting your pivot model to an actual Eloquent model.
Custom Pivot Models and Incrementing IDs
If you have defined a many-to-many relationship that uses a custom pivot model, and that pivot model has an auto-incrementing primary key, you should ensure your custom pivot model class defines an incrementing property that is set to true.
/**
* Indicates if the IDs are auto-incrementing.
*
* @var bool
*/
public $incrementing = true;
多型關聯 (Polymorphic Relationships)
多型關聯允許子模型使用單個關聯屬於多種模型類型。例如,想像你正在建立一個允許使用者分享部落格文章和影片的應用程式。在這樣的應用程式中,Comment 模型可能同時屬於 Post 和 Video 模型。
一對一 (多型) (One to One (Polymorphic)) (One to One)
資料表結構 (Table Structure)
一對一多型關聯類似於典型的一對一關聯;但是,子模型可以使用單個關聯屬於多種模型類型。例如,部落格 Post 和 User 可能共享與 Image 模型的多型關聯。使用一對一多型關聯允許你有一個唯一的圖片資料表,這些圖片可能與文章和使用者相關聯。首先,讓我們檢查資料表結構:
posts
id - integer
name - string
users
id - integer
name - string
images
id - integer
url - string
imageable_id - integer
imageable_type - string
注意 images 資料表上的 imageable_id 和 imageable_type 欄位。imageable_id 欄位將包含文章或使用者的 ID 值,而 imageable_type 欄位將包含父模型的類別名稱。Eloquent 使用 imageable_type 欄位來確定在存取 imageable 關聯時回傳哪種「類型」的父模型。在這種情況下,該欄位將包含 App\Models\Post 或 App\Models\User。
模型結構 (Model Structure)
接下來,讓我們檢查建立此關聯所需的模型定義:
<?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');
}
}
檢索關聯 (Retrieving the Relationship)
一旦定義了資料庫資料表和模型,你就可以通過模型存取關聯。例如,要檢索文章的圖片,我們可以存取 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 關聯將回傳 Post 或 User 實例,具體取決於哪種類型的模型擁有該圖片。
鍵名慣例 (Key Conventions)
如果有必要,你可以指定多型子模型使用的「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');
}
一對多 (多型) (One to Many (Polymorphic)) (One to Many)
資料表結構 (Table Structure)
一對多多型關聯類似於典型的一對多關聯;但是,子模型可以使用單個關聯屬於多種模型類型。例如,想像你的應用程式的使用者可以對文章和影片進行「評論」。使用多型關聯,你可以使用單個 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
模型結構 (Model Structure)
接下來,讓我們檢查建立此關聯所需的模型定義:
<?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');
}
}
檢索關聯 (Retrieving the Relationship)
一旦定義了資料庫資料表和模型,你就可以通過模型的動態關聯屬性存取關聯。例如,要存取文章的所有評論,我們可以使用 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 關聯將回傳 Post 或 Video 實例,具體取決於哪種類型的模型是評論的父模型。
自動為子模型水合父模型 (Automatically Hydrating Parent Models on Children)
即使使用 Eloquent 動態載入,如果在迴圈子模型時試著從子模型存取父模型,也會產生「N + 1」查詢問題:
$posts = Post::with('comments')->get();
foreach ($posts as $post) {
foreach ($post->comments as $comment) {
echo $comment->commentable->title;
}
}
在上面的範例中,引入了「N + 1」查詢問題,因為即使為每個 Post 模型預先載入了評論,Eloquent 也不會自動在每個子 Comment 模型上水合父 Post。
如果你希望 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();
一對多之其一 (多型) (One of Many (Polymorphic)) (One of Many)
有時一個模型可能有多個相關模型,但你希望輕鬆檢索關聯中「最新」或「最舊」的相關模型。例如,一個 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 方法接受可排序的欄位作為其第一個參數,以及在查詢相關模型時要應用的聚合函數(min 或 max):
/**
* Get the user's most popular image.
*/
public function bestImage(): MorphOne
{
return $this->morphOne(Image::class, 'imageable')->ofMany('likes', 'max');
}
[!NOTE] 可以建構更進階的「一對多之其一」關聯。有關更多資訊,請參閱 進階的一對多之其一關聯 文件。
多對多 (多型) (Many to Many (Polymorphic)) (Many to Many)
資料表結構 (Table Structure)
多對多多型關聯比「morph one」和「morph many」關聯稍微複雜一些。例如,一個 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
[!NOTE] 在深入研究多型多對多關聯之前,你可能會從閱讀典型的 多對多關聯 文件中受益。
模型結構 (Model Structure)
接下來,我們準備在模型上定義關聯。Post 和 Video 模型都將包含一個呼叫 Eloquent 基底模型類別提供的 morphToMany 方法的 tags 方法。
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');
}
}
定義關聯的反向 (Defining the Inverse of the Relationship)
接下來,在 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');
}
}
檢索關聯 (Retrieving the Relationship)
一旦定義了資料庫資料表和模型,你就可以通過模型存取關聯。例如,要存取文章的所有標籤,你可以使用 tags 動態關聯屬性:
use App\Models\Post;
$post = Post::find(1);
foreach ($post->tags as $tag) {
// ...
}
你可以通過存取執行 morphedByMany 呼叫的方法名稱來從多型子模型檢索多型關聯的父模型。在這種情況下,即 Tag 模型上的 posts 或 videos 方法:
use App\Models\Tag;
$tag = Tag::find(1);
foreach ($tag->posts as $post) {
// ...
}
foreach ($tag->videos as $video) {
// ...
}
自訂多型類型 (Custom Polymorphic Types)
預設情況下,Laravel 將使用完全限定的類別名稱來儲存相關模型的「類型」。例如,鑑於上面的一對多關聯範例,其中 Comment 模型可能屬於 Post 或 Video 模型,預設的 commentable_type 將分別是 App\Models\Post 或 App\Models\Video。但是,你可能希望將這些值與應用程式的內部結構解耦。
例如,我們可以使用簡單的字串(如 post 和 video)代替使用模型名稱作為「類型」。這樣做,即使模型被重新命名,資料庫中的多型「類型」欄位值也將保持有效:
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);
[!WARNING] 當向現有應用程式新增「morph map」時,資料庫中每個仍包含完全限定類別的 morphable
*_type欄位值都需要轉換為其「map」名稱。
動態關聯 (Dynamic Relationships)
你可以使用 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');
});
[!WARNING] 定義動態關聯時,請務必為 Eloquent 關聯方法提供明確的鍵名參數。
查詢關聯 (Querying Relations)
由於所有 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 子句 (Chaining orWhere Clauses After (Chaining Orwhere Clauses After Relationships)
Relationships)
如上例所示,你可以在查詢關聯時自由地向關聯新增額外的約束。但是,將 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)
關聯方法與動態屬性 (Relationship Methods vs. Dynamic Properties)
如果你不需要向 Eloquent 關聯查詢新增額外的約束,你可以像存取屬性一樣存取關聯。例如,繼續使用我們的 User 和 Post 範例模型,我們可以像這樣存取使用者的所有文章:
use App\Models\User;
$user = User::find(1);
foreach ($user->posts as $post) {
// ...
}
動態關聯屬性執行「延遲載入」,這意味著它們只有在你實際存取它們時才會載入其關聯數據。因此,開發人員經常使用 預先載入 來預先載入他們知道在載入模型後將被存取的關聯。預先載入顯著減少了載入模型關聯必須執行的 SQL 查詢。
查詢關聯存在性 (Querying Relationship Existence)
檢索模型記錄時,你可能希望根據關聯的存在限制結果。例如,想像你想要檢索所有至少有一條評論的部落格文章。為此,你可以將關聯名稱傳遞給 has 和 orHas 方法:
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();
如果你需要更強大的功能,可以使用 whereHas 和 orWhereHas 方法在 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();
[!WARNING] Eloquent 目前不支援跨資料庫查詢關聯存在性。關聯必須存在於同一個資料庫中。
多對多關聯存在性查詢 (Many to Many Relationship Existence Queries)
whereAttachedTo 方法可用於查詢與模型或模型集合具有多對多附件的模型:
$users = User::whereAttachedTo($role)->get();
你也可以提供一個 集合 實例給 whereAttachedTo 方法。這樣做時,Laravel 將檢索附加到集合中任何模型的模型:
$tags = Tag::whereLike('name', '%laravel%')->get();
$posts = Post::whereAttachedTo($tags)->get();
內聯關聯存在性查詢 (Inline Relationship Existence Queries)
如果你想查詢附加到關聯查詢的單個、簡單 where 條件的關聯存在性,你可能會發現使用 whereRelation、orWhereRelation、whereMorphRelation 和 orWhereMorphRelation 方法更方便。例如,我們可以查詢所有有未批准評論的文章:
use App\Models\Post;
$posts = Post::whereRelation('comments', 'is_approved', false)->get();
當然,就像呼叫查詢建構器的 where 方法一樣,你也可以指定一個運算符:
$posts = Post::whereRelation(
'comments', 'created_at', '>=', now()->subHour()
)->get();
查詢關聯不存在性 (Querying Relationship Absence)
檢索模型記錄時,你可能希望根據關聯的不存在限制結果。例如,想像你想要檢索所有沒有任何評論的部落格文章。為此,你可以將關聯名稱傳遞給 doesntHave 和 orDoesntHave 方法:
use App\Models\Post;
$posts = Post::doesntHave('comments')->get();
如果你需要更強大的功能,可以使用 whereDoesntHave 和 orWhereDoesntHave 方法向 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', 1);
})->get();
查詢 Morph To 關聯 (Querying Morph To Relationships)
要查詢「morph to」關聯的存在性,你可以使用 whereHasMorph 和 whereDoesntHaveMorph 方法。這些方法接受關聯名稱作為其第一個參數。接下來,這些方法接受你希望包含在查詢中的相關模型的名稱。最後,你可以提供一個閉包來自訂關聯查詢:
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」關聯父級的子級。你可以使用 whereMorphedTo 和 whereNotMorphedTo 方法來實現這一點,這些方法將自動確定給定模型的正確 morph 類型映射。這些方法接受 morphTo 關聯的名稱作為其第一個參數,並接受相關父模型作為其第二個參數:
$comments = Comment::whereMorphedTo('commentable', $post)
->orWhereMorphedTo('commentable', $video)
->get();
查詢所有相關模型 (Querying All Related Models)
你可以提供 * 作為萬用字元值,而不是傳遞可能的 polymorphic 模型陣列。這將指示 Laravel 從資料庫中檢索所有可能的 polymorphic 類型。Laravel 將執行額外的查詢以執行此操作:
use Illuminate\Database\Eloquent\Builder;
$comments = Comment::whereHasMorph('commentable', '*', function (Builder $query) {
$query->where('title', 'like', 'foo%');
})->get();
聚合相關模型 (Aggregating Related Models)
計算相關模型數量 (Counting Related Models)
有時你可能想要計算給定關聯的相關模型數量,而無需實際載入模型。為此,你可以使用 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;
延遲計數載入 (Deferred Count Loading)
使用 loadCount 方法,你可以在父模型已被檢索後載入關聯計數:
$book = Book::first();
$book->loadCount('genres');
如果你需要在計數查詢上設定額外的查詢約束,你可以傳遞一個以你希望計數的關聯為鍵的陣列。陣列值應該是接收查詢建構器實例的閉包:
$book->loadCount(['reviews' => function (Builder $query) {
$query->where('rating', 5);
}])
關聯計數與自訂 Select 語句 (Relationship Counting and Custom Select (Relationship Counting And Custom Select Statements)
Statements)
如果你將 withCount 與 select 語句結合使用,請確保在 select 方法之後呼叫 withCount:
$posts = Post::select(['title', 'body'])
->withCount('comments')
->get();
其他聚合函數 (Other Aggregate Functions)
除了 withCount 方法之外,Eloquent 還提供了 withMin、withMax、withAvg、withSum 和 withExists 方法。這些方法將在你的結果模型上放置一個 {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 關聯的相關模型數量 (Counting Related Models on Morph To (Counting Related Models on Morph To Relationships)
Relationships)
如果你想要預先載入「morph to」關聯,以及該關聯可能回傳的各種實體的相關模型計數,你可以結合使用 with 方法和 morphTo 關聯的 morphWithCount 方法。
在這個範例中,讓我們假設 Photo 和 Post 模型可以建立 ActivityFeed 模型。我們將假設 ActivityFeed 模型定義了一個名為 parentable 的「morph to」關聯,該關聯允許我們檢索給定 ActivityFeed 實例的父 Photo 或 Post 模型。此外,讓我們假設 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();
延遲計數載入 (Deferred Count Loading)
讓我們假設我們已經檢索了一組 ActivityFeed 模型,現在我們想要載入與活動動態關聯的各種 parentable 模型的巢狀關聯計數。你可以使用 loadMorphCount 方法來實現此目的:
$activities = ActivityFeed::with('parentable')->get();
$activities->loadMorphCount('parentable', [
Photo::class => ['tags'],
Post::class => ['comments'],
]);
預先載入 (Eager Loading)
當作為屬性存取 Eloquent 關聯時,相關模型是「延遲載入」的。這意味著直到你第一次存取該屬性時,關聯資料才會被實際載入。但是,Eloquent 可以在查詢父模型時「預先載入」關聯。預先載入緩解了「N + 1」查詢問題。為了解釋 N + 1 查詢問題,考慮一個 Book 模型「屬於」一個 Author 模型:
<?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, ...)
預先載入多個關聯 (Eager Loading Multiple Relationships)
有時你可能需要預先載入幾個不同的關聯。為此,只需將關聯陣列傳遞給 with 方法:
$books = Book::with(['author', 'publisher'])->get();
巢狀預先載入 (Nested Eager Loading)
要預先載入關聯的關聯,你可以使用「點」語法。例如,讓我們預先載入所有書籍的作者以及所有作者的個人聯絡人:
$books = Book::with('author.contacts')->get();
或者,你可以通過向 with 方法提供巢狀陣列來指定巢狀預先載入關聯,這在預先載入多個巢狀關聯時很方便:
$books = Book::with([
'author' => [
'contacts',
'publisher',
],
])->get();
巢狀預先載入 morphTo 關聯 (Nested Eager Loading morphTo Relationships)
如果你想要預先載入 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();
}
}
在這個範例中,讓我們假設 Event、Photo 和 Post 模型可以建立 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();
預先載入特定欄位 (Eager Loading Specific Columns)
你可能並不總是需要從你正在檢索的關聯中獲取每一欄。因此,Eloquent 允許你指定你想要檢索的關聯欄位:
$books = Book::with('author:id,name,book_id')->get();
[!WARNING] 使用此功能時,你應該始終在要檢索的欄位列表中包含
id欄位和任何相關的外鍵欄位。
預設預先載入 (Eager Loading by Default)
有時你可能希望在檢索模型時始終載入某些關聯。為此,你可以在模型上定義一個 $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();
限制預先載入 (Constraining Eager Loads)
有時你可能希望預先載入關聯,但也為預先載入查詢指定額外的查詢條件。你可以通過將關聯陣列傳遞給 with 方法來實現此目的,其中陣列鍵是關聯名稱,陣列值是向預先載入查詢新增額外約束的閉包:
use App\Models\User;
$users = User::with(['posts' => function ($query) {
$query->where('title', 'like', '%code%');
}])->get();
在這個範例中,Eloquent 將僅預先載入文章 title 欄位包含單詞 code 的文章。你可以呼叫其他 查詢建構器 方法來進一步自訂預先載入操作:
$users = User::with(['posts' => function ($query) {
$query->orderBy('created_at', 'desc');
}])->get();
限制 morphTo 關聯的預先載入 (Constraining Eager Loading of morphTo (Constraining Eager Loading Of Morph To Relationships)
Relationships)
如果你正在預先載入 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」的影片。
使用關聯存在性限制預先載入 (Constraining Eager Loads With Relationship (Constraining Eager Loads With Relationship Existence)
Existence)
你有時可能會發現自己需要檢查關聯的存在性,同時根據相同的條件載入關聯。例如,你可能希望僅檢索具有符合給定查詢條件的子 Post 模型的 User 模型,同時也預先載入符合條件的文章。你可以使用 withWhereHas 方法來實現此目的:
use App\Models\User;
$users = User::withWhereHas('posts', function ($query) {
$query->where('featured', true);
})->get();
延遲預先載入 (Lazy Eager Loading)
有時你可能需要在父模型已被檢索後預先載入關聯。例如,如果你需要動態決定是否載入相關模型,這可能很有用:
use App\Models\Book;
$books = Book::all();
if ($condition) {
$books->load('author', 'publisher');
}
如果你需要在預先載入查詢上設定額外的查詢約束,你可以傳遞一個以你希望載入的關聯為鍵的陣列。陣列值應該是接收查詢實例的閉包實例:
$author->load(['books' => function ($query) {
$query->orderBy('published_date', 'asc');
}]);
要僅在關聯尚未載入時載入它,請使用 loadMissing 方法:
$book->loadMissing('author');
巢狀延遲預先載入和 morphTo (Nested Lazy Eager Loading and 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();
}
}
在這個範例中,讓我們假設 Event、Photo 和 Post 模型可以建立 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'],
]);
自動預先載入 (Automatic Eager Loading)
[!WARNING] 此功能目前處於測試階段,以收集社群回饋。即使在修補程式版本中,此功能的行為和功能也可能會發生變化。
在許多情況下,Laravel 可以自動預先載入你存取的關聯。要啟用自動預先載入,你應該在應用程式的 AppServiceProvider 的 boot 方法中呼叫 Model::automaticallyEagerLoadRelationships 方法:
use Illuminate\Database\Eloquent\Model;
/**
* Bootstrap any application services.
*/
public function boot(): void
{
Model::automaticallyEagerLoadRelationships();
}
啟用此功能後,Laravel 將嘗試自動載入你存取的任何尚未載入的關聯。例如,考慮以下情況:
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 將自動 延遲預先載入 使用者集合中所有使用者的文章。同樣,當你嘗試存取任何已檢索文章的評論時,將為所有最初檢索的文章延遲預先載入所有評論。
如果你不想全域啟用自動預先載入,你仍然可以通過在集合上呼叫 withRelationshipAutoloading 方法為單個 Eloquent 集合實例啟用此功能:
$users = User::where('vip', true)->get();
return $users->withRelationshipAutoloading();
防止延遲載入 (Preventing Lazy Loading)
如前所述,預先載入關聯通常可以為你的應用程式提供顯著的效能優勢。因此,如果你願意,你可以指示 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}].");
});
插入和更新相關模型 (Inserting and Updating Related Models)
save 方法 (The save Method)
Eloquent 提供了方便的方法來將新模型新增到關聯中。例如,也許你需要向文章新增新評論。除了手動設定 Comment 模型上的 post_id 屬性外,你還可以使用關聯的 save 方法插入評論:
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.']),
]);
save 和 saveMany 方法將持久化給定的模型實例,但不會將新持久化的模型新增到已載入到父模型上的任何記憶體中關聯。如果你計劃在使用 save 或 saveMany 方法後存取關聯,你可能希望使用 refresh 方法重新載入模型及其關聯:
$post->comments()->save($comment);
$post->refresh();
// All comments, including the newly saved comment...
$post->comments;
遞迴儲存模型和關聯 (Recursively Saving Models and Relationships)
如果你想 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 方法 (The create Method)
除了 save 和 saveMany 方法之外,你還可以使用 create 方法,該方法接受屬性陣列,建立模型並將其插入資料庫。save 和 create 之間的區別在於,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.'],
]);
createQuietly 和 createManyQuietly 方法可用於建立模型而不分派任何事件:
$user = User::find(1);
$user->posts()->createQuietly([
'title' => 'Post title.',
]);
$user->posts()->createManyQuietly([
['title' => 'First post.'],
['title' => 'Second post.'],
]);
你也可以使用 findOrNew、firstOrNew、firstOrCreate 和 updateOrCreate 方法來 在關聯上建立和更新模型。
[!NOTE] 在使用
create方法之前,請務必查看 批量賦值 文件。
屬於關聯 (Belongs To Relationships)
如果你想將子模型分配給新的父模型,可以使用 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();
多對多關聯 (Many to Many Relationships)
附加 / 解除 (Attaching / Detaching)
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();
為了方便起見,attach 和 detach 也接受 ID 陣列作為輸入:
$user = User::find(1);
$user->roles()->detach([1, 2, 3]);
$user->roles()->attach([
1 => ['expires' => $expires],
2 => ['expires' => $expires],
]);
同步關聯 (Syncing Associations)
你也可以使用 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]);
切換關聯 (Toggling Associations)
多對多關聯也提供了一個 toggle 方法,該方法「切換」給定相關模型 ID 的附加狀態。如果給定的 ID 當前已附加,它將被解除。同樣,如果它當前已解除,它將被附加:
$user->roles()->toggle([1, 2, 3]);
你也可以傳遞額外的中間資料表值與 ID:
$user->roles()->toggle([
1 => ['expires' => true],
2 => ['expires' => true],
]);
更新中間資料表上的記錄 (Updating a Record on the Intermediate Table)
如果你需要更新關聯中間資料表中的現有行,可以使用 updateExistingPivot 方法。此方法接受中間記錄外鍵和要更新的屬性陣列:
$user = User::find(1);
$user->roles()->updateExistingPivot($roleId, [
'active' => false,
]);
觸碰父級時間戳記 (Touching Parent Timestamps)
當一個模型定義了與另一個模型的 belongsTo 或 belongsToMany 關聯時,例如 Comment 屬於 Post,在子模型更新時更新父級的時間戳記有時很有幫助。
例如,當 Comment 模型更新時,你可能希望自動「觸碰」擁有的 Post 的 updated_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);
}
}
[!WARNING] Parent model timestamps will only be updated if the child model is updated using Eloquent's
savemethod.