跳轉至內容

Eloquent: 關聯

簡介

資料庫表之間經常存在關聯。例如,一篇部落格文章可能有很多評論,或者一個訂單可能與下訂單的使用者相關聯。Eloquent 使管理和處理這些關聯變得簡單,並支援多種常見的關聯型別

定義關聯

Eloquent 關聯被定義為 Eloquent 模型類中的方法。由於關聯同時也作為強大的查詢構建器,將關聯定義為方法可以提供強大的方法鏈式呼叫和查詢能力。例如,我們可以對這個 posts 關聯鏈式新增額外的查詢約束

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

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

一對一 / Has One

一對一關聯是一種非常基礎的資料庫關聯型別。例如,一個 User 模型可能與一個 Phone 模型關聯。要定義此關聯,我們將 phone 方法放在 User 模型中。phone 方法應該呼叫 hasOne 方法並返回其結果。hasOne 方法透過模型的 Illuminate\Database\Eloquent\Model 基類提供給您的模型

1<?php
2 
3namespace App\Models;
4 
5use Illuminate\Database\Eloquent\Model;
6use Illuminate\Database\Eloquent\Relations\HasOne;
7 
8class User extends Model
9{
10 /**
11 * Get the phone associated with the user.
12 */
13 public function phone(): HasOne
14 {
15 return $this->hasOne(Phone::class);
16 }
17}

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

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

Eloquent 根據父模型名稱確定關聯的外部索引鍵。在這種情況下,Phone 模型被自動假定擁有一個 user_id 外部索引鍵。如果您希望覆蓋此約定,可以向 hasOne 方法傳遞第二個引數

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

此外,Eloquent 假定外部索引鍵的值應該與父模型的主鍵列匹配。換句話說,Eloquent 將在 Phone 記錄的 user_id 列中查詢使用者 id 列的值。如果您希望關聯使用除 id 或您模型主鍵之外的主鍵值,您可以向 hasOne 方法傳遞第三個引數

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

定義關聯的反向

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

1<?php
2 
3namespace App\Models;
4 
5use Illuminate\Database\Eloquent\Model;
6use Illuminate\Database\Eloquent\Relations\BelongsTo;
7 
8class Phone extends Model
9{
10 /**
11 * Get the user that owns the phone.
12 */
13 public function user(): BelongsTo
14 {
15 return $this->belongsTo(User::class);
16 }
17}

呼叫 user 方法時,Eloquent 將嘗試查詢一個 idPhone 模型上的 user_id 列匹配的 User 模型。

Eloquent 透過檢查關聯方法的名稱並將方法名稱字尾 _id 來確定外部索引鍵名稱。因此,在這種情況下,Eloquent 假定 Phone 模型具有 user_id 列。但是,如果 Phone 模型上的外部索引鍵不是 user_id,您可以將自定義鍵名作為第二個引數傳遞給 belongsTo 方法

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

如果父模型不使用 id 作為其主鍵,或者您希望使用不同的列來查詢關聯模型,您可以向 belongsTo 方法傳遞第三個引數,指定父表的自定義鍵

1/**
2 * Get the user that owns the phone.
3 */
4public function user(): BelongsTo
5{
6 return $this->belongsTo(User::class, 'foreign_key', 'owner_key');
7}

一對多 / Has Many

一對多關聯用於定義單個模型作為零個或多個子模型的父模型的關聯。例如,一篇部落格文章可以有無數條評論。像所有其他 Eloquent 關聯一樣,一對多關聯是透過在您的 Eloquent 模型中定義一個方法來定義的

1<?php
2 
3namespace App\Models;
4 
5use Illuminate\Database\Eloquent\Model;
6use Illuminate\Database\Eloquent\Relations\HasMany;
7 
8class Post extends Model
9{
10 /**
11 * Get the comments for the blog post.
12 */
13 public function comments(): HasMany
14 {
15 return $this->hasMany(Comment::class);
16 }
17}

請記住,Eloquent 會自動為 Comment 模型確定正確的外部索引鍵列。按照慣例,Eloquent 會取父模型的“蛇形命名法”名稱並加上 _id 字尾。因此,在此示例中,Eloquent 將假定 Comment 模型上的外部索引鍵列是 post_id

定義了關聯方法後,我們可以透過訪問 comments 屬性來訪問關聯評論的集合。請記住,由於 Eloquent 提供了“動態關聯屬性”,我們可以像訪問模型上定義的屬性一樣訪問關聯方法

1use App\Models\Post;
2 
3$comments = Post::find(1)->comments;
4 
5foreach ($comments as $comment) {
6 // ...
7}

由於所有關聯也充當查詢構建器,您可以透過呼叫 comments 方法並繼續將條件連結到查詢上來向關聯查詢新增進一步的約束

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

hasOne 方法一樣,您也可以透過向 hasMany 方法傳遞附加引數來覆蓋外部索引鍵和本地鍵

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

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

即使在使用 Eloquent 渴求式載入時,如果您在迴圈遍歷子模型時嘗試從子模型訪問父模型,也可能出現“N + 1”查詢問題

1$posts = Post::with('comments')->get();
2 
3foreach ($posts as $post) {
4 foreach ($post->comments as $comment) {
5 echo $comment->post->title;
6 }
7}

在上面的示例中,引入了“N + 1”查詢問題,因為即使為每個 Post 模型渴求式載入了評論,Eloquent 也不會自動在每個子 Comment 模型上填充父 Post

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

1<?php
2 
3namespace App\Models;
4 
5use Illuminate\Database\Eloquent\Model;
6use Illuminate\Database\Eloquent\Relations\HasMany;
7 
8class Post extends Model
9{
10 /**
11 * Get the comments for the blog post.
12 */
13 public function comments(): HasMany
14 {
15 return $this->hasMany(Comment::class)->chaperone();
16 }
17}

或者,如果您希望在執行時選擇自動填充父模型,您可以在渴求式載入關聯時呼叫 chaperone 模型

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

一對多(反向) / Belongs To

現在我們可以訪問帖子的所有評論了,讓我們定義一個關聯,允許評論訪問其父帖子。要定義 hasMany 關聯的反向,請在子模型上定義一個呼叫 belongsTo 方法的關聯方法

1<?php
2 
3namespace App\Models;
4 
5use Illuminate\Database\Eloquent\Model;
6use Illuminate\Database\Eloquent\Relations\BelongsTo;
7 
8class Comment extends Model
9{
10 /**
11 * Get the post that owns the comment.
12 */
13 public function post(): BelongsTo
14 {
15 return $this->belongsTo(Post::class);
16 }
17}

定義關聯後,我們可以透過訪問 post “動態關聯屬性”來檢索評論的父帖子

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

在上面的示例中,Eloquent 將嘗試查詢一個 idComment 模型上的 post_id 列匹配的 Post 模型。

Eloquent 透過檢查關聯方法的名稱並加上 _ 後跟父模型主鍵列名稱的字尾來確定預設外部索引鍵名稱。因此,在此示例中,Eloquent 將假定 comments 表上的 Post 模型的外部索引鍵是 post_id

但是,如果您的關聯外部索引鍵不遵循這些約定,您可以將自定義外部索引鍵名稱作為第二個引數傳遞給 belongsTo 方法

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

如果您的父模型不使用 id 作為其主鍵,或者您希望使用不同的列來查詢關聯模型,您可以向 belongsTo 方法傳遞第三個引數,指定父表的自定義鍵

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

預設模型

belongsTohasOnehasOneThroughmorphOne 關聯允許您定義一個預設模型,如果給定的關聯為 null,則返回該模型。這種模式通常被稱為 空物件模式,可以幫助消除程式碼中的條件檢查。在下面的示例中,如果 Post 模型沒有附加使用者,user 關聯將返回一個空的 App\Models\User 模型

1/**
2 * Get the author of the post.
3 */
4public function user(): BelongsTo
5{
6 return $this->belongsTo(User::class)->withDefault();
7}

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

1/**
2 * Get the author of the post.
3 */
4public function user(): BelongsTo
5{
6 return $this->belongsTo(User::class)->withDefault([
7 'name' => 'Guest Author',
8 ]);
9}
10 
11/**
12 * Get the author of the post.
13 */
14public function user(): BelongsTo
15{
16 return $this->belongsTo(User::class)->withDefault(function (User $user, Post $post) {
17 $user->name = 'Guest Author';
18 });
19}

查詢 Belongs To 關聯

在查詢“belongs to”關聯的子項時,您可以手動構建 where 子句來檢索相應的 Eloquent 模型

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

但是,您可能會發現使用 whereBelongsTo 方法更方便,它將自動為給定的模型確定正確的關聯和外部索引鍵

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

您還可以向 whereBelongsTo 方法提供一個 集合 例項。執行此操作時,Laravel 將檢索屬於集合中任何父模型的模型

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

預設情況下,Laravel 將根據模型的類名確定與給定模型關聯的關聯;但是,您可以透過將關聯名稱作為 whereBelongsTo 方法的第二個引數提供來手動指定關聯名稱

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

Has One of Many

有時一個模型可能有許多關聯模型,但您想輕鬆檢索關聯的“最新”或“最舊”的關聯模型。例如,User 模型可能與許多 Order 模型相關聯,但您想定義一種便捷的方式來與使用者下的最新訂單進行互動。您可以使用 hasOne 關聯型別結合 ofMany 方法來實現這一點

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

同樣,您可以定義一個方法來檢索關聯的“最舊”或第一個關聯模型

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

預設情況下,latestOfManyoldestOfMany 方法將根據模型的主鍵(必須是可排序的)檢索最新或最舊的關聯模型。但是,有時您可能希望使用不同的排序標準從較大的關聯中檢索單個模型。

例如,使用 ofMany 方法,您可以檢索使用者最昂貴的訂單。ofMany 方法接受可排序的列作為其第一個引數,以及在查詢關聯模型時應用的聚合函式(minmax

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

由於 PostgreSQL 不支援對 UUID 列執行 MAX 函式,目前無法將 one-of-many 關聯與 PostgreSQL UUID 列結合使用。

將“多”關聯轉換為 Has One 關聯

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

1/**
2 * Get the user's orders.
3 */
4public function orders(): HasMany
5{
6 return $this->hasMany(Order::class);
7}
8 
9/**
10 * Get the user's largest order.
11 */
12public function largestOrder(): HasOne
13{
14 return $this->orders()->one()->ofMany('price', 'max');
15}

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

1public function latestDeployment(): HasOneThrough
2{
3 return $this->deployments()->one()->latestOfMany();
4}

高階 Has One of Many 關聯

可以構建更高階的“has one of many”關聯。例如,Product 模型可能有許多關聯的 Price 模型,即使在新價格釋出後也會保留在系統中。此外,產品的定價新資料可能會提前釋出,透過 published_at 列在未來日期生效。

因此,總之,我們需要檢索釋出日期不在未來的最新發布定價。此外,如果兩個價格具有相同的釋出日期,我們將優先選擇具有最大 ID 的價格。為了實現這一點,我們必須向 ofMany 方法傳遞一個包含決定最新價格的可排序列的陣列。此外,閉包將作為第二個引數提供給 ofMany 方法。此閉包負責向關聯查詢新增額外的釋出日期約束

1/**
2 * Get the current pricing for the product.
3 */
4public function currentPricing(): HasOne
5{
6 return $this->hasOne(Price::class)->ofMany([
7 'published_at' => 'max',
8 'id' => 'max',
9 ], function (Builder $query) {
10 $query->where('published_at', '<', now());
11 });
12}

Has One Through

“has-one-through”關聯定義了與另一個模型的 一對一 關聯。但是,此關聯表明宣告模型可以透過 經過 第三個模型與另一個模型的例項相匹配。

例如,在車輛維修店應用程式中,每個 Mechanic 模型可以與一個 Car 模型關聯,而每個 Car 模型可以與一個 Owner 模型關聯。雖然機械師和所有者在資料庫中沒有直接關聯,但機械師可以 透過 Car 模型訪問所有者。讓我們看看定義此關聯所需的表

1mechanics
2 id - integer
3 name - string
4
5cars
6 id - integer
7 model - string
8 mechanic_id - integer
9
10owners
11 id - integer
12 name - string
13 car_id - integer

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

1<?php
2 
3namespace App\Models;
4 
5use Illuminate\Database\Eloquent\Model;
6use Illuminate\Database\Eloquent\Relations\HasOneThrough;
7 
8class Mechanic extends Model
9{
10 /**
11 * Get the car's owner.
12 */
13 public function carOwner(): HasOneThrough
14 {
15 return $this->hasOneThrough(Owner::class, Car::class);
16 }
17}

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

或者,如果相關的關聯已經在所有參與關聯的模型上定義,您可以透過呼叫 through 方法並提供這些關聯的名稱來流暢地定義“has-one-through”關聯。例如,如果 Mechanic 模型有一個 cars 關聯,而 Car 模型有一個 owner 關聯,您可以這樣定義連線機械師和所有者的“has-one-through”關聯

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

關鍵約定

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

1class Mechanic extends Model
2{
3 /**
4 * Get the car's owner.
5 */
6 public function carOwner(): HasOneThrough
7 {
8 return $this->hasOneThrough(
9 Owner::class,
10 Car::class,
11 'mechanic_id', // Foreign key on the cars table...
12 'car_id', // Foreign key on the owners table...
13 'id', // Local key on the mechanics table...
14 'id' // Local key on the cars table...
15 );
16 }
17}

或者,如前所述,如果相關的關聯已經在所有參與關聯的模型上定義,您可以透過呼叫 through 方法並提供這些關聯的名稱來流暢地定義“has-one-through”關聯。這種方法提供了重用現有關聯上已定義的關鍵約定的優勢

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

Has Many Through

“has-many-through”關聯提供了一種透過中間關聯訪問遠端關聯的便捷方式。例如,假設我們正在構建一個像 Laravel Cloud 這樣的部署平臺。Application 模型可以透過中間的 Environment 模型訪問許多 Deployment 模型。使用此示例,您可以輕鬆地為給定的應用程式收集所有部署。讓我們看看定義此關聯所需的表

1applications
2 id - integer
3 name - string
4
5environments
6 id - integer
7 application_id - integer
8 name - string
9
10deployments
11 id - integer
12 environment_id - integer
13 commit_hash - string

既然我們已經檢查了關聯的表結構,讓我們在 Application 模型上定義關聯

1<?php
2 
3namespace App\Models;
4 
5use Illuminate\Database\Eloquent\Model;
6use Illuminate\Database\Eloquent\Relations\HasManyThrough;
7 
8class Application extends Model
9{
10 /**
11 * Get all of the deployments for the application.
12 */
13 public function deployments(): HasManyThrough
14 {
15 return $this->hasManyThrough(Deployment::class, Environment::class);
16 }
17}

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

或者,如果相關的關聯已經在所有參與關聯的模型上定義,您可以透過呼叫 through 方法並提供這些關聯的名稱來流暢地定義“has-many-through”關聯。例如,如果 Application 模型有一個 environments 關聯,而 Environment 模型有一個 deployments 關聯,您可以這樣定義連線應用程式和部署的“has-many-through”關聯

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

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

關鍵約定

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

1class Application extends Model
2{
3 public function deployments(): HasManyThrough
4 {
5 return $this->hasManyThrough(
6 Deployment::class,
7 Environment::class,
8 'application_id', // Foreign key on the environments table...
9 'environment_id', // Foreign key on the deployments table...
10 'id', // Local key on the applications table...
11 'id' // Local key on the environments table...
12 );
13 }
14}

或者,如前所述,如果相關的關聯已經在所有參與關聯的模型上定義,您可以透過呼叫 through 方法並提供這些關聯的名稱來流暢地定義“has-many-through”關聯。這種方法提供了重用現有關聯上已定義的關鍵約定的優勢

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

作用域關聯

在模型中新增限制關聯的額外方法是很常見的。例如,您可以在 User 模型中新增一個 featuredPosts 方法,該方法透過額外的 where 約束來限制更廣泛的 posts 關聯

1<?php
2 
3namespace App\Models;
4 
5use Illuminate\Database\Eloquent\Model;
6use Illuminate\Database\Eloquent\Relations\HasMany;
7 
8class User extends Model
9{
10 /**
11 * Get the user's posts.
12 */
13 public function posts(): HasMany
14 {
15 return $this->hasMany(Post::class)->latest();
16 }
17 
18 /**
19 * Get the user's featured posts.
20 */
21 public function featuredPosts(): HasMany
22 {
23 return $this->posts()->where('featured', true);
24 }
25}

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

1/**
2 * Get the user's featured posts.
3 */
4public function featuredPosts(): HasMany
5{
6 return $this->posts()->withAttributes(['featured' => true]);
7}

withAttributes 方法將使用給定的屬性向查詢新增 where 條件,並將給定的屬性新增到透過該關聯建立的任何模型中

1$post = $user->featuredPosts()->create(['title' => 'Featured Post']);
2 
3$post->featured; // true

要指示 withAttributes 方法不要向查詢新增 where 條件,您可以將 asConditions 引數設定為 false

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

多對多關聯

多對多關聯比 hasOnehasMany 關聯稍微複雜一些。多對多關聯的一個例子是一個使用者擁有多個角色,而這些角色也被應用程式中的其他使用者共享。例如,一個使用者可能被分配了“作者”和“編輯”角色;但是,這些角色也可能被分配給其他使用者。因此,一個使用者有多個角色,一個角色有多個使用者。

表結構

要定義此關聯,需要三個資料庫表:usersrolesrole_userrole_user 表派生自相關模型名稱的字母順序,幷包含 user_idrole_id 列。該表用作連線使用者和角色的中間表。

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

1users
2 id - integer
3 name - string
4
5roles
6 id - integer
7 name - string
8
9role_user
10 user_id - integer
11 role_id - integer

模型結構

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

1<?php
2 
3namespace App\Models;
4 
5use Illuminate\Database\Eloquent\Model;
6use Illuminate\Database\Eloquent\Relations\BelongsToMany;
7 
8class User extends Model
9{
10 /**
11 * The roles that belong to the user.
12 */
13 public function roles(): BelongsToMany
14 {
15 return $this->belongsToMany(Role::class);
16 }
17}

定義關聯後,您可以使用 roles 動態關聯屬性訪問使用者的角色

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

由於所有關聯也充當查詢構建器,您可以透過呼叫 roles 方法並繼續將條件連結到查詢上來向關聯查詢新增進一步的約束

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

為了確定關聯中間表的表名,Eloquent 將按字母順序連線兩個相關模型名稱。但是,您可以隨意覆蓋此約定。您可以向 belongsToMany 方法傳遞第二個引數來實現

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

除了自定義中間表的名稱外,您還可以透過向 belongsToMany 方法傳遞附加引數來自定義表上鍵的列名。第三個引數是您定義關聯的模型的外部索引鍵名稱,而第四個引數是您要連線的模型的外部索引鍵名稱

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

定義關聯的反向

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

1<?php
2 
3namespace App\Models;
4 
5use Illuminate\Database\Eloquent\Model;
6use Illuminate\Database\Eloquent\Relations\BelongsToMany;
7 
8class Role extends Model
9{
10 /**
11 * The users that belong to the role.
12 */
13 public function users(): BelongsToMany
14 {
15 return $this->belongsToMany(User::class);
16 }
17}

如您所見,該關聯的定義方式與其 User 模型對應項完全相同,只是引用了 App\Models\User 模型。由於我們重用了 belongsToMany 方法,在定義多對多關聯的“反向”時,所有常見的表和鍵自定義選項都可用。

檢索中間表列

正如您已經學到的,處理多對多關聯需要中間表的存在。Eloquent 提供了一些非常有用的與該表互動的方法。例如,假設我們的 User 模型關聯了許多 Role 模型。在訪問此關聯後,我們可以使用模型上的 pivot 屬性訪問中間表

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

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

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

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

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

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

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

自定義 pivot 屬性名稱

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

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

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

指定自定義中間表屬性後,您可以使用自定義名稱訪問中間表資料

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

透過中間表列過濾查詢

您還可以在定義關聯時使用 wherePivotwherePivotInwherePivotNotInwherePivotBetweenwherePivotNotBetweenwherePivotNullwherePivotNotNull 方法來過濾 belongsToMany 關聯查詢返回的結果

1return $this->belongsToMany(Role::class)
2 ->wherePivot('approved', 1);
3 
4return $this->belongsToMany(Role::class)
5 ->wherePivotIn('priority', [1, 2]);
6 
7return $this->belongsToMany(Role::class)
8 ->wherePivotNotIn('priority', [1, 2]);
9 
10return $this->belongsToMany(Podcast::class)
11 ->as('subscriptions')
12 ->wherePivotBetween('created_at', ['2020-01-01 00:00:00', '2020-12-31 00:00:00']);
13 
14return $this->belongsToMany(Podcast::class)
15 ->as('subscriptions')
16 ->wherePivotNotBetween('created_at', ['2020-01-01 00:00:00', '2020-12-31 00:00:00']);
17 
18return $this->belongsToMany(Podcast::class)
19 ->as('subscriptions')
20 ->wherePivotNull('expired_at');
21 
22return $this->belongsToMany(Podcast::class)
23 ->as('subscriptions')
24 ->wherePivotNotNull('expired_at');

wherePivot 向查詢新增 where 子句約束,但在透過定義的關聯建立新模型時不會新增指定的值。如果您需要使用特定的 pivot 值來查詢和建立關聯,您可以使用 withPivotValue 方法

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

透過中間表列排序查詢

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

1return $this->belongsToMany(Badge::class)
2 ->where('rank', 'gold')
3 ->orderByPivotDesc('created_at');

定義自定義中間表模型

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

自定義多對多 pivot 模型應擴充套件 Illuminate\Database\Eloquent\Relations\Pivot 類,而自定義多型多對多 pivot 模型應擴充套件 Illuminate\Database\Eloquent\Relations\MorphPivot 類。例如,我們可以定義一個使用自定義 RoleUser pivot 模型的 Role 模型

1<?php
2 
3namespace App\Models;
4 
5use Illuminate\Database\Eloquent\Model;
6use Illuminate\Database\Eloquent\Relations\BelongsToMany;
7 
8class Role extends Model
9{
10 /**
11 * The users that belong to the role.
12 */
13 public function users(): BelongsToMany
14 {
15 return $this->belongsToMany(User::class)->using(RoleUser::class);
16 }
17}

定義 RoleUser 模型時,您應該擴充套件 Illuminate\Database\Eloquent\Relations\Pivot

1<?php
2 
3namespace App\Models;
4 
5use Illuminate\Database\Eloquent\Relations\Pivot;
6 
7class RoleUser extends Pivot
8{
9 // ...
10}

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

自定義 Pivot 模型和自增 ID

如果您定義了一個使用自定義 pivot 模型的多對多關聯,並且該 pivot 模型具有自增主鍵,則應確保您的自定義 pivot 模型類使用 Table 屬性並將 incrementing 設定為 true

1use Illuminate\Database\Eloquent\Attributes\Table;
2use Illuminate\Database\Eloquent\Relations\Pivot;
3 
4#[Table(incrementing: true)]
5class RoleUser extends Pivot
6{
7 // ...
8}

多型關聯

多型關聯允許子模型使用單個關聯屬於不止一種型別的模型。例如,想象一下您正在構建一個允許使用者分享部落格文章和影片的應用程式。在這種應用程式中,Comment 模型可能同時屬於 PostVideo 模型。

一對一(多型)

表結構

一對一多型關聯類似於典型的一對一關聯;但是,子模型可以使用單個關聯屬於不止一種型別的模型。例如,部落格 PostUser 可以共享與 Image 模型的 多型 關聯。使用一對一多型關聯,您可以擁有一個唯一的圖片表,該表可以與帖子和使用者關聯。首先,讓我們檢查表結構

1posts
2 id - integer
3 name - string
4
5users
6 id - integer
7 name - string
8
9images
10 id - integer
11 url - string
12 imageable_id - integer
13 imageable_type - string

注意 images 表上的 imageable_idimageable_type 列。imageable_id 列將包含帖子或使用者的 ID 值,而 imageable_type 列將包含父模型的類名。imageable_type 列被 Eloquent 用於確定在訪問 imageable 關聯時要返回哪種“型別”的父模型。在這種情況下,該列將包含 App\Models\PostApp\Models\User

模型結構

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

1<?php
2 
3namespace App\Models;
4 
5use Illuminate\Database\Eloquent\Model;
6use Illuminate\Database\Eloquent\Relations\MorphTo;
7 
8class Image extends Model
9{
10 /**
11 * Get the parent imageable model (user or post).
12 */
13 public function imageable(): MorphTo
14 {
15 return $this->morphTo();
16 }
17}
18 
19use Illuminate\Database\Eloquent\Model;
20use Illuminate\Database\Eloquent\Relations\MorphOne;
21 
22class Post extends Model
23{
24 /**
25 * Get the post's image.
26 */
27 public function image(): MorphOne
28 {
29 return $this->morphOne(Image::class, 'imageable');
30 }
31}
32 
33use Illuminate\Database\Eloquent\Model;
34use Illuminate\Database\Eloquent\Relations\MorphOne;
35 
36class User extends Model
37{
38 /**
39 * Get the user's image.
40 */
41 public function image(): MorphOne
42 {
43 return $this->morphOne(Image::class, 'imageable');
44 }
45}

檢索關聯

定義資料庫表和模型後,您可以透過模型訪問關聯。例如,要檢索帖子的圖片,我們可以訪問 image 動態關聯屬性

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

您可以透過訪問執行 morphTo 呼叫的方法的名稱來檢索多型模型的父級。在這種情況下,那是 Image 模型上的 imageable 方法。因此,我們將該方法作為動態關聯屬性進行訪問

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

Image 模型上的 imageable 關聯將返回 PostUser 例項,具體取決於哪種型別的模型擁有該圖片。

關鍵約定

如有必要,您可以指定多型子模型使用的“id”和“type”列的名稱。如果這樣做,請確保始終將關聯的名稱作為第一個引數傳遞給 morphTo 方法。通常,此值應與方法名稱匹配,因此您可以使用 PHP 的 __FUNCTION__ 常量

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

一對多(多型)

表結構

一對多多型關聯類似於典型的一對多關聯;但是,子模型可以使用單個關聯屬於不止一種型別的模型。例如,想象一下您的應用程式的使用者可以對帖子和影片進行“評論”。使用多型關聯,您可以使用單個 comments 表來包含帖子和影片的評論。首先,讓我們檢查構建此關聯所需的表結構

1posts
2 id - integer
3 title - string
4 body - text
5
6videos
7 id - integer
8 title - string
9 url - string
10
11comments
12 id - integer
13 body - text
14 commentable_id - integer
15 commentable_type - string

模型結構

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

1<?php
2 
3namespace App\Models;
4 
5use Illuminate\Database\Eloquent\Model;
6use Illuminate\Database\Eloquent\Relations\MorphTo;
7 
8class Comment extends Model
9{
10 /**
11 * Get the parent commentable model (post or video).
12 */
13 public function commentable(): MorphTo
14 {
15 return $this->morphTo();
16 }
17}
18 
19use Illuminate\Database\Eloquent\Model;
20use Illuminate\Database\Eloquent\Relations\MorphMany;
21 
22class Post extends Model
23{
24 /**
25 * Get all of the post's comments.
26 */
27 public function comments(): MorphMany
28 {
29 return $this->morphMany(Comment::class, 'commentable');
30 }
31}
32 
33use Illuminate\Database\Eloquent\Model;
34use Illuminate\Database\Eloquent\Relations\MorphMany;
35 
36class Video extends Model
37{
38 /**
39 * Get all of the video's comments.
40 */
41 public function comments(): MorphMany
42 {
43 return $this->morphMany(Comment::class, 'commentable');
44 }
45}

檢索關聯

定義資料庫表和模型後,您可以透過模型的動態關聯屬性訪問關聯。例如,要訪問帖子的所有評論,我們可以使用 comments 動態屬性

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

您還可以透過訪問執行 morphTo 呼叫的方法的名稱來檢索多型子模型的父級。在這種情況下,那是 Comment 模型上的 commentable 方法。因此,我們將該方法作為動態關聯屬性進行訪問,以便訪問評論的父模型

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

Comment 模型上的 commentable 關聯將返回 PostVideo 例項,具體取決於哪種型別的模型是評論的父級。

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

即使在使用 Eloquent 渴求式載入時,如果您在迴圈遍歷子模型時嘗試從子模型訪問父模型,也可能出現“N + 1”查詢問題

1$posts = Post::with('comments')->get();
2 
3foreach ($posts as $post) {
4 foreach ($post->comments as $comment) {
5 echo $comment->commentable->title;
6 }
7}

在上面的示例中,引入了“N + 1”查詢問題,因為即使為每個 Post 模型渴求式載入了評論,Eloquent 也不會自動在每個子 Comment 模型上填充父 Post

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

1class Post extends Model
2{
3 /**
4 * Get all of the post's comments.
5 */
6 public function comments(): MorphMany
7 {
8 return $this->morphMany(Comment::class, 'commentable')->chaperone();
9 }
10}

或者,如果您希望在執行時選擇自動填充父模型,您可以在渴求式載入關聯時呼叫 chaperone 模型

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

One of Many(多型)

有時一個模型可能有許多關聯模型,但您想輕鬆檢索關聯的“最新”或“最舊”的關聯模型。例如,User 模型可能與許多 Image 模型相關聯,但您想定義一種便捷的方式來與使用者上傳的最新圖片進行互動。您可以使用 morphOne 關聯型別結合 ofMany 方法來實現這一點

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

同樣,您可以定義一個方法來檢索關聯的“最舊”或第一個關聯模型

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

預設情況下,latestOfManyoldestOfMany 方法將根據模型的主鍵(必須是可排序的)檢索最新或最舊的關聯模型。但是,有時您可能希望使用不同的排序標準從較大的關聯中檢索單個模型。

例如,使用 ofMany 方法,您可以檢索使用者最“喜歡”的圖片。ofMany 方法接受可排序的列作為其第一個引數,以及在查詢關聯模型時應用的聚合函式(minmax

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

可以構建更高階的“one of many”關聯。有關更多資訊,請查閱 has one of many 文件

多對多(多型)

表結構

多對多多型關聯比“morph one”和“morph many”關聯稍微複雜一些。例如,Post 模型和 Video 模型可以共享與 Tag 模型的 多型 關聯。在這種情況下使用多對多多型關聯將允許您的應用程式擁有一個唯一的標籤表,該表可以與帖子或影片關聯。首先,讓我們檢查構建此關聯所需的表結構

1posts
2 id - integer
3 name - string
4
5videos
6 id - integer
7 name - string
8
9tags
10 id - integer
11 name - string
12
13taggables
14 tag_id - integer
15 taggable_id - integer
16 taggable_type - string

在深入瞭解多型多對多關聯之前,閱讀有關典型 多對多關聯 的文件可能會對您有所幫助。

模型結構

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

morphToMany 方法接受關聯模型的名稱以及“關聯名稱”。基於我們分配給中間表名稱及其包含的鍵,我們將此關聯稱為“taggable”

1<?php
2 
3namespace App\Models;
4 
5use Illuminate\Database\Eloquent\Model;
6use Illuminate\Database\Eloquent\Relations\MorphToMany;
7 
8class Post extends Model
9{
10 /**
11 * Get all of the tags for the post.
12 */
13 public function tags(): MorphToMany
14 {
15 return $this->morphToMany(Tag::class, 'taggable');
16 }
17}

定義關聯的反向

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

morphedByMany 方法接受關聯模型的名稱以及“關聯名稱”。基於我們分配給中間表名稱及其包含的鍵,我們將此關聯稱為“taggable”

1<?php
2 
3namespace App\Models;
4 
5use Illuminate\Database\Eloquent\Model;
6use Illuminate\Database\Eloquent\Relations\MorphToMany;
7 
8class Tag extends Model
9{
10 /**
11 * Get all of the posts that are assigned this tag.
12 */
13 public function posts(): MorphToMany
14 {
15 return $this->morphedByMany(Post::class, 'taggable');
16 }
17 
18 /**
19 * Get all of the videos that are assigned this tag.
20 */
21 public function videos(): MorphToMany
22 {
23 return $this->morphedByMany(Video::class, 'taggable');
24 }
25}

檢索關聯

定義資料庫表和模型後,您可以透過模型訪問關聯。例如,要訪問帖子的所有標籤,您可以使用 tags 動態關聯屬性

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

您可以透過訪問執行 morphedByMany 呼叫的方法的名稱從多型子模型中檢索多型關聯的父級。在這種情況下,那是 Tag 模型上的 postsvideos 方法

1use App\Models\Tag;
2 
3$tag = Tag::find(1);
4 
5foreach ($tag->posts as $post) {
6 // ...
7}
8 
9foreach ($tag->videos as $video) {
10 // ...
11}

自定義多型型別

預設情況下,Laravel 將使用完全限定的類名來儲存相關模型的“型別”。例如,鑑於上面的一對多關聯示例(Comment 模型可能屬於 PostVideo 模型),預設的 commentable_type 將分別是 App\Models\PostApp\Models\Video。但是,您可能希望將這些值與應用程式的內部結構解耦。

例如,我們不使用模型名稱作為“型別”,而是使用簡單的字串,例如 postvideo。透過這樣做,即使模型被重新命名,我們資料庫中的多型“型別”列值也將保持有效

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

您可以在 App\Providers\AppServiceProvider 類的 boot 方法中呼叫 enforceMorphMap 方法,或者根據需要建立一個單獨的服務提供者。

您可以在執行時使用模型的 getMorphClass 方法確定給定模型的多型別名。相反,您可以使用 Relation::getMorphedModel 方法確定與多型別名關聯的完全限定類名

1use Illuminate\Database\Eloquent\Relations\Relation;
2 
3$alias = $post->getMorphClass();
4 
5$class = Relation::getMorphedModel($alias);

向現有應用程式新增“morph map”時,資料庫中每個仍包含完全限定類名的多型 *_type 列值都需要轉換為其“對映”名稱。

動態關聯

您可以使用 resolveRelationUsing 方法在執行時定義 Eloquent 模型之間的關聯。雖然通常不建議用於常規應用程式開發,但這在開發 Laravel 包時偶爾會有用。

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

1use App\Models\Order;
2use App\Models\Customer;
3 
4Order::resolveRelationUsing('customer', function (Order $orderModel) {
5 return $orderModel->belongsTo(Customer::class, 'customer_id');
6});

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

查詢關聯

由於所有 Eloquent 關聯都是透過方法定義的,您可以呼叫這些方法來獲取關聯例項,而無需實際執行查詢來載入關聯模型。此外,所有型別的 Eloquent 關聯也充當 查詢構建器,允許您在最終對資料庫執行 SQL 查詢之前繼續將約束鏈式新增到關聯查詢中。

例如,想象一個部落格應用程式,其中 User 模型關聯了許多 Post 模型

1<?php
2 
3namespace App\Models;
4 
5use Illuminate\Database\Eloquent\Model;
6use Illuminate\Database\Eloquent\Relations\HasMany;
7 
8class User extends Model
9{
10 /**
11 * Get all of the posts for the user.
12 */
13 public function posts(): HasMany
14 {
15 return $this->hasMany(Post::class);
16 }
17}

您可以查詢 posts 關聯並像這樣新增額外的關聯約束

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

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

在關聯後鏈式使用 orWhere 子句

如上例所示,您可以在查詢關聯時自由新增額外的約束。但是,在向關聯新增 orWhere 子句時要小心,因為 orWhere 子句將在與關聯約束相同的級別上進行邏輯分組

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

上面的示例將生成以下 SQL。如您所見,or 子句指示查詢返回 任何 投票數大於 100 的帖子。查詢不再侷限於特定使用者

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

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

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

上面的示例將產生以下 SQL。請注意,邏輯分組已正確對約束進行了分組,並且查詢仍然侷限於特定使用者

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

關聯方法 vs 動態屬性

如果您不需要向 Eloquent 關聯查詢新增額外的約束,您可以像訪問屬性一樣訪問關聯。例如,繼續使用我們的 UserPost 示例模型,我們可以這樣訪問使用者的所有帖子

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

動態關聯屬性執行“延遲載入”,這意味著它們只會在您實際訪問它們時載入其關聯資料。因此,開發人員經常使用 渴求式載入 來預載入他們知道在載入模型後會被訪問的關聯。渴求式載入顯著減少了載入模型關聯必須執行的 SQL 查詢數量。

查詢關聯是否存在

檢索模型記錄時,您可能希望根據關聯的存在與否來限制結果。例如,想象您想要檢索所有至少有一條評論的部落格文章。為此,您可以將關聯名稱傳遞給 hasorHas 方法

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

您還可以指定運算子和計數值以進一步自定義查詢

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

可以使用“點”符號構建巢狀的 has 語句。例如,您可以檢索所有至少有一條至少有一個圖片的評論的帖子

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

如果您需要更強大的功能,可以使用 whereHasorWhereHas 方法在 has 查詢上定義額外的查詢約束,例如檢查評論的內容

1use Illuminate\Database\Eloquent\Builder;
2 
3// Retrieve posts with at least one comment containing words like code%...
4$posts = Post::whereHas('comments', function (Builder $query) {
5 $query->where('content', 'like', 'code%');
6})->get();
7 
8// Retrieve posts with at least ten comments containing words like code%...
9$posts = Post::whereHas('comments', function (Builder $query) {
10 $query->where('content', 'like', 'code%');
11}, '>=', 10)->get();

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

多對多關聯存在性查詢

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

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

您還可以向 whereAttachedTo 方法提供一個 集合 例項。執行此操作時,Laravel 將檢索與集合中任何模型關聯的模型

1$tags = Tag::whereLike('name', '%laravel%')->get();
2 
3$posts = Post::whereAttachedTo($tags)->get();

內聯關聯存在性查詢

如果您想查詢關聯的存在性並在關聯查詢上新增單個簡單的 where 條件,您可能會發現使用 whereRelationorWhereRelationwhereMorphRelationorWhereMorphRelation 方法更方便。例如,我們可以查詢所有有未批准評論的帖子

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

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

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

查詢關聯是否缺失

檢索模型記錄時,您可能希望根據關聯的缺失來限制結果。例如,想象您想要檢索所有 沒有 任何評論的部落格文章。為此,您可以將關聯名稱傳遞給 doesntHaveorDoesntHave 方法

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

如果您需要更強大的功能,可以使用 whereDoesntHaveorWhereDoesntHave 方法向您的 doesntHave 查詢新增額外的查詢約束,例如檢查評論的內容

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

您可以使用“點”符號對巢狀關聯執行查詢。例如,以下查詢將檢索沒有評論的帖子以及有評論但其中沒有評論來自被封停用戶的帖子

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

查詢 Morph To 關聯

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

1use App\Models\Comment;
2use App\Models\Post;
3use App\Models\Video;
4use Illuminate\Database\Eloquent\Builder;
5 
6// Retrieve comments associated to posts or videos with a title like code%...
7$comments = Comment::whereHasMorph(
8 'commentable',
9 [Post::class, Video::class],
10 function (Builder $query) {
11 $query->where('title', 'like', 'code%');
12 }
13)->get();
14 
15// Retrieve comments associated to posts with a title not like code%...
16$comments = Comment::whereDoesntHaveMorph(
17 'commentable',
18 Post::class,
19 function (Builder $query) {
20 $query->where('title', 'like', 'code%');
21 }
22)->get();

您可能偶爾需要根據相關多型模型的“型別”新增查詢約束。傳遞給 whereHasMorph 方法的閉包可能會接收到一個 $type 值作為其第二個引數。此引數允許您檢查正在構建的查詢的“型別”

1use Illuminate\Database\Eloquent\Builder;
2 
3$comments = Comment::whereHasMorph(
4 'commentable',
5 [Post::class, Video::class],
6 function (Builder $query, string $type) {
7 $column = $type === Post::class ? 'content' : 'title';
8 
9 $query->where($column, 'like', 'code%');
10 }
11)->get();

有時您可能想查詢“morph to”關聯的父級的子級。您可以使用 whereMorphedTowhereNotMorphedTo 方法來實現,它們將自動為給定模型確定正確的 morph 型別對映。這些方法接受 morphTo 關聯的名稱作為它們的第一個引數,並將相關父模型作為它們的第二個引數

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

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

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

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

1use App\Models\Post;
2 
3$posts = Post::withCount('comments')->get();
4 
5foreach ($posts as $post) {
6 echo $post->comments_count;
7}

透過向 withCount 方法傳遞陣列,您可以新增多個關聯的“計數”並向查詢新增額外的約束

1use Illuminate\Database\Eloquent\Builder;
2 
3$posts = Post::withCount(['votes', 'comments' => function (Builder $query) {
4 $query->where('content', 'like', 'code%');
5}])->get();
6 
7echo $posts[0]->votes_count;
8echo $posts[0]->comments_count;

您還可以對關聯計數結果設定別名,從而允許在同一個關聯上進行多次計數

1use Illuminate\Database\Eloquent\Builder;
2 
3$posts = Post::withCount([
4 'comments',
5 'comments as pending_comments_count' => function (Builder $query) {
6 $query->where('approved', false);
7 },
8])->get();
9 
10echo $posts[0]->comments_count;
11echo $posts[0]->pending_comments_count;

延遲計數載入

使用 loadCount 方法,您可以在檢索父模型後加載關聯計數

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

如果您需要在計數查詢上設定額外的查詢約束,您可以傳遞一個以您想要統計的關聯為鍵的陣列。陣列值應該是接收查詢構建器例項的閉包

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

關聯計數和自定義 Select 語句

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

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

其他聚合函式

除了 withCount 方法外,Eloquent 還提供了 withMinwithMaxwithAvgwithSumwithExists 方法。這些方法將在結果模型上放置一個 {relation}_{function}_{column} 屬性

1use App\Models\Post;
2 
3$posts = Post::withSum('comments', 'votes')->get();
4 
5foreach ($posts as $post) {
6 echo $post->comments_sum_votes;
7}

如果您希望使用其他名稱訪問聚合函式的結果,您可以指定自己的別名

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

loadCount 方法一樣,這些方法的延遲版本也可用。這些額外的聚合操作可以在已經檢索到的 Eloquent 模型上執行

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

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

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

如果您希望渴求式載入“morph to”關聯,以及該關聯可能返回的各種實體的關聯模型計數,您可以結合使用 with 方法和 morphTo 關聯的 morphWithCount 方法。

在此示例中,讓我們假設 PhotoPost 模型可以建立 ActivityFeed 模型。我們將假定 ActivityFeed 模型定義了一個名為 parentable 的“morph to”關聯,它允許我們為給定的 ActivityFeed 例項檢索父級 PhotoPost 模型。此外,讓我們假設 Photo 模型“has many” Tag 模型,而 Post 模型“has many” Comment 模型。

現在,讓我們想象我們要檢索 ActivityFeed 例項併為每個 ActivityFeed 例項渴求式載入 parentable 父模型。此外,我們要檢索與每個父級照片關聯的標籤數量以及與每個父級帖子關聯的評論數量

1use Illuminate\Database\Eloquent\Relations\MorphTo;
2 
3$activities = ActivityFeed::with([
4 'parentable' => function (MorphTo $morphTo) {
5 $morphTo->morphWithCount([
6 Photo::class => ['tags'],
7 Post::class => ['comments'],
8 ]);
9 }])->get();

延遲計數載入

假設我們已經檢索了一組 ActivityFeed 模型,現在我們想要為與活動流關聯的各種 parentable 模型載入巢狀關聯計數。您可以使用 loadMorphCount 方法來實現這一點

1$activities = ActivityFeed::with('parentable')->get();
2 
3$activities->loadMorphCount('parentable', [
4 Photo::class => ['tags'],
5 Post::class => ['comments'],
6]);

渴求式載入

當作為屬性訪問 Eloquent 關聯時,相關模型會被“延遲載入”。這意味著關聯資料直到您首次訪問該屬性時才會被實際載入。但是,Eloquent 可以在您查詢父模型時“渴求式載入”關聯。渴求式載入緩解了“N + 1”查詢問題。為了說明 N + 1 查詢問題,請考慮一個“belongs to”一個 Author 模型的 Book 模型

1<?php
2 
3namespace App\Models;
4 
5use Illuminate\Database\Eloquent\Model;
6use Illuminate\Database\Eloquent\Relations\BelongsTo;
7 
8class Book extends Model
9{
10 /**
11 * Get the author that wrote the book.
12 */
13 public function author(): BelongsTo
14 {
15 return $this->belongsTo(Author::class);
16 }
17}

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

1use App\Models\Book;
2 
3$books = Book::all();
4 
5foreach ($books as $book) {
6 echo $book->author->name;
7}

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

值得慶幸的是,我們可以使用渴求式載入將此操作減少到僅兩個查詢。構建查詢時,您可以使用 with 方法指定應渴求式載入哪些關聯

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

對於此操作,將只執行兩個查詢 - 一個查詢用於檢索所有書籍,一個查詢用於檢索所有書籍的所有作者

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

渴求式載入多個關聯

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

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

巢狀渴求式載入

要渴求式載入關聯的關聯,您可以使用“點”語法。例如,讓我們渴求式載入所有書籍的作者以及作者的所有個人聯絡方式

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

或者,您可以透過向 with 方法提供巢狀陣列來指定巢狀的渴求式載入關聯,當渴求式載入多個巢狀關聯時,這非常方便

1$books = Book::with([
2 'author' => [
3 'contacts',
4 'publisher',
5 ],
6])->get();

巢狀渴求式載入 morphTo 關聯

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

1<?php
2 
3use Illuminate\Database\Eloquent\Model;
4use Illuminate\Database\Eloquent\Relations\MorphTo;
5 
6class ActivityFeed extends Model
7{
8 /**
9 * Get the parent of the activity feed record.
10 */
11 public function parentable(): MorphTo
12 {
13 return $this->morphTo();
14 }
15}

在此示例中,讓我們假設 EventPhotoPost 模型可以建立 ActivityFeed 模型。此外,讓我們假設 Event 模型屬於一個 Calendar 模型,Photo 模型與 Tag 模型關聯,而 Post 模型屬於一個 Author 模型。

使用這些模型定義和關聯,我們可以檢索 ActivityFeed 模型例項並渴求式載入所有 parentable 模型及其各自的巢狀關聯

1use Illuminate\Database\Eloquent\Relations\MorphTo;
2 
3$activities = ActivityFeed::query()
4 ->with(['parentable' => function (MorphTo $morphTo) {
5 $morphTo->morphWith([
6 Event::class => ['calendar'],
7 Photo::class => ['tags'],
8 Post::class => ['author'],
9 ]);
10 }])->get();

渴求式載入特定列

您可能並不總是需要檢索關聯中的每一列。因此,Eloquent 允許您指定要檢索關聯的哪些列

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

使用此功能時,應始終在要檢索的列列表中包含 id 列和任何相關的外部索引鍵列。

預設渴求式載入

有時您可能希望在檢索模型時總是載入某些關聯。為此,您可以在模型上定義一個 $with 屬性

1<?php
2 
3namespace App\Models;
4 
5use Illuminate\Database\Eloquent\Model;
6use Illuminate\Database\Eloquent\Relations\BelongsTo;
7 
8class Book extends Model
9{
10 /**
11 * The relationships that should always be loaded.
12 *
13 * @var array
14 */
15 protected $with = ['author'];
16 
17 /**
18 * Get the author that wrote the book.
19 */
20 public function author(): BelongsTo
21 {
22 return $this->belongsTo(Author::class);
23 }
24 
25 /**
26 * Get the genre of the book.
27 */
28 public function genre(): BelongsTo
29 {
30 return $this->belongsTo(Genre::class);
31 }
32}

如果您想從單次查詢的 $with 屬性中刪除一項,您可以使用 without 方法

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

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

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

約束渴求式載入

有時您可能希望渴求式載入一個關聯,但也為渴求式載入查詢指定額外的查詢條件。您可以透過向 with 方法傳遞關聯陣列來實現這一點,其中陣列鍵是關聯名稱,陣列值是向渴求式載入查詢新增額外約束的閉包

1use App\Models\User;
2 
3$users = User::with(['posts' => function ($query) {
4 $query->where('title', 'like', '%code%');
5}])->get();

在此示例中,Eloquent 將僅渴求式載入帖子 title 列包含單詞 code 的帖子。您可以呼叫其他 查詢構建器 方法來進一步自定義渴求式載入操作

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

約束 morphTo 關聯的渴求式載入

如果您正在渴求式載入 morphTo 關聯,Eloquent 將執行多個查詢來獲取每種型別的相關模型。您可以使用 MorphTo 關聯的 constrain 方法向這些查詢中的每一個新增額外的約束

1use Illuminate\Database\Eloquent\Relations\MorphTo;
2 
3$comments = Comment::with(['commentable' => function (MorphTo $morphTo) {
4 $morphTo->constrain([
5 Post::class => function ($query) {
6 $query->whereNull('hidden_at');
7 },
8 Video::class => function ($query) {
9 $query->where('type', 'educational');
10 },
11 ]);
12}])->get();

在此示例中,Eloquent 將僅渴求式載入未隱藏的帖子和 type 值為“educational”的影片。

透過關聯存在性約束渴求式載入

有時您可能會發現自己需要檢查關聯的存在性,同時根據相同的條件載入該關聯。例如,您可能希望僅檢索具有與給定查詢條件匹配的子 Post 模型的 User 模型,同時也渴求式載入匹配的帖子。您可以使用 withWhereHas 方法來實現這一點

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

延遲渴求式載入

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

1use App\Models\Book;
2 
3$books = Book::all();
4 
5if ($condition) {
6 $books->load('author', 'publisher');
7}

如果您需要在渴求式載入查詢上設定額外的查詢約束,您可以傳遞一個以您想要載入的關聯為鍵的陣列。陣列值應該是接收查詢例項的閉包例項

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

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

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

巢狀延遲渴求式載入和 morphTo

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

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

1<?php
2 
3use Illuminate\Database\Eloquent\Model;
4use Illuminate\Database\Eloquent\Relations\MorphTo;
5 
6class ActivityFeed extends Model
7{
8 /**
9 * Get the parent of the activity feed record.
10 */
11 public function parentable(): MorphTo
12 {
13 return $this->morphTo();
14 }
15}

在此示例中,讓我們假設 EventPhotoPost 模型可以建立 ActivityFeed 模型。此外,讓我們假設 Event 模型屬於一個 Calendar 模型,Photo 模型與 Tag 模型關聯,而 Post 模型屬於一個 Author 模型。

使用這些模型定義和關聯,我們可以檢索 ActivityFeed 模型例項並渴求式載入所有 parentable 模型及其各自的巢狀關聯

1$activities = ActivityFeed::with('parentable')
2 ->get()
3 ->loadMorph('parentable', [
4 Event::class => ['calendar'],
5 Photo::class => ['tags'],
6 Post::class => ['author'],
7 ]);

自動渴求式載入

此功能目前處於測試階段,旨在收集社群反饋。此功能的功能和行為即使在補丁釋出中也可能會發生變化。

在許多情況下,Laravel 可以自動渴求式載入您訪問的關聯。要啟用自動渴求式載入,您應該在應用程式 AppServiceProviderboot 方法中呼叫 Model::automaticallyEagerLoadRelationships 方法

1use Illuminate\Database\Eloquent\Model;
2 
3/**
4 * Bootstrap any application services.
5 */
6public function boot(): void
7{
8 Model::automaticallyEagerLoadRelationships();
9}

啟用此功能後,Laravel 將嘗試自動載入您訪問的任何尚未載入的關聯。例如,考慮以下場景

1use App\Models\User;
2 
3$users = User::all();
4 
5foreach ($users as $user) {
6 foreach ($user->posts as $post) {
7 foreach ($post->comments as $comment) {
8 echo $comment->content;
9 }
10 }
11}

通常,上面的程式碼會為每個使用者執行一個查詢以檢索他們的帖子,併為每個帖子執行一個查詢以檢索其評論。但是,當啟用了 automaticallyEagerLoadRelationships 功能時,當您嘗試訪問任何檢索到的使用者上的帖子時,Laravel 將自動 延遲渴求式載入 使用者集合中所有使用者的帖子。同樣,當您嘗試訪問任何檢索到的帖子上的評論時,將為最初檢索的所有帖子延遲渴求式載入所有評論。

如果您不想全域性啟用自動渴求式載入,您仍然可以透過對集合呼叫 withRelationshipAutoloading 方法來為單個 Eloquent 集合例項啟用此功能

1$users = User::where('vip', true)->get();
2 
3return $users->withRelationshipAutoloading();

防止延遲載入

如前所述,渴求式載入關聯通常可以為您的應用程式提供顯著的效能優勢。因此,如果您願意,您可以指示 Laravel 總是防止關聯的延遲載入。為此,您可以呼叫基 Eloquent 模型類提供的 preventLazyLoading 方法。通常,您應該在應用程式 AppServiceProvider 類的 boot 方法中呼叫此方法。

preventLazyLoading 方法接受一個可選的布林引數,該引數指示是否應防止延遲載入。例如,您可能希望僅在非生產環境中停用延遲載入,以便您的生產環境即使在意外存在延遲載入的關聯時也能正常執行

1use Illuminate\Database\Eloquent\Model;
2 
3/**
4 * Bootstrap any application services.
5 */
6public function boot(): void
7{
8 Model::preventLazyLoading(! $this->app->isProduction());
9}

防止延遲載入後,當您的應用程式嘗試延遲載入任何 Eloquent 關聯時,Eloquent 將丟擲 Illuminate\Database\LazyLoadingViolationException 異常。

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

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

save 方法

Eloquent 提供了向關聯新增新模型的便捷方法。例如,也許您需要向帖子新增新評論。您可以插入評論,而不是手動在 Comment 模型上設定 post_id 屬性,使用關聯的 save 方法

1use App\Models\Comment;
2use App\Models\Post;
3 
4$comment = new Comment(['message' => 'A new comment.']);
5 
6$post = Post::find(1);
7 
8$post->comments()->save($comment);

請注意,我們沒有將 comments 關聯作為動態屬性進行訪問。相反,我們呼叫了 comments 方法來獲取關聯的例項。save 方法將自動將適當的 post_id 值新增到新的 Comment 模型中。

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

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

savesaveMany 方法將持久化給定的模型例項,但不會將新持久化的模型新增到已載入到父模型上的任何記憶體中關聯中。如果您計劃在使用 savesaveMany 方法後訪問該關聯,您可能希望使用 refresh 方法重新載入模型及其關聯

1$post->comments()->save($comment);
2 
3$post->refresh();
4 
5// All comments, including the newly saved comment...
6$post->comments;

遞迴儲存模型和關聯

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

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

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

1$post->pushQuietly();

create 方法

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

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

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

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

createQuietlycreateManyQuietly 方法可用於建立模型,而不分發任何事件

1$user = User::find(1);
2 
3$user->posts()->createQuietly([
4 'title' => 'Post title.',
5]);
6 
7$user->posts()->createManyQuietly([
8 ['title' => 'First post.'],
9 ['title' => 'Second post.'],
10]);

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

在使用 create 方法之前,請務必檢視 批次賦值 文件。

Belongs To 關聯

如果您想將子模型分配給新的父模型,可以使用 associate 方法。在此示例中,User 模型定義了對 Account 模型的 belongsTo 關聯。此 associate 方法將在子模型上設定外部索引鍵

1use App\Models\Account;
2 
3$account = Account::find(10);
4 
5$user->account()->associate($account);
6 
7$user->save();

要從子模型中刪除父模型,可以使用 dissociate 方法。此方法將關聯的外部索引鍵設定為 null

1$user->account()->dissociate();
2 
3$user->save();

多對多關聯

附加/分離

Eloquent 還提供了使處理多對多關聯更加方便的方法。例如,想象一個使用者可以擁有多個角色,而一個角色可以擁有多個使用者。您可以使用 attach 方法透過在關聯的中間表中插入記錄將角色附加到使用者

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

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

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

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

1// Detach a single role from the user...
2$user->roles()->detach($roleId);
3 
4// Detach all roles from the user...
5$user->roles()->detach();

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

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

同步關聯

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

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

您還可以將附加的中間表值與 ID 一起傳遞

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

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

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

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

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

切換關聯

多對多關聯還提供了一個 toggle 方法,它“切換”給定關聯模型 ID 的附加狀態。如果給定的 ID 當前已附加,它將被分離。同樣,如果它當前已分離,它將被附加

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

您還可以將附加的中間表值與 ID 一起傳遞

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

更新中間表上的記錄

如果您需要更新關聯的中間表中的現有行,您可以使用 updateExistingPivot 方法。此方法接受中間記錄外部索引鍵和要更新的屬性陣列

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

觸發父級時間戳更新

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

例如,更新 Comment 模型時,您可能希望自動“觸控”擁有它的 Postupdated_at 時間戳,以便將其設定為當前日期和時間。為此,您可以在子模型上使用 Touches 屬性,其中包含在更新子模型時應更新其 updated_at 時間戳的關聯名稱

1<?php
2 
3namespace App\Models;
4 
5use Illuminate\Database\Eloquent\Attributes\Touches;
6use Illuminate\Database\Eloquent\Model;
7use Illuminate\Database\Eloquent\Relations\BelongsTo;
8 
9#[Touches(['post'])]
10class Comment extends Model
11{
12 /**
13 * Get the post that the comment belongs to.
14 */
15 public function post(): BelongsTo
16 {
17 return $this->belongsTo(Post::class);
18 }
19}

僅當使用 Eloquent 的 save 方法更新子模型時,才會更新父模型時間戳。