Eloquent:關聯
簡介
資料庫中的資料表通常會互相彼此關聯。舉例來說,部落格文章可能會有許多的留言,而訂單則可能會關聯到建立訂單的使用者。在 Eloquent 中,要管理並處理這些關聯非常簡單,並支援多種常見的關聯:
定義關聯
Eloquent 關聯是作為方法定義在 Eloquent Model 類別中。由於關聯也可當作強大的 Query Builder 使用,因此將關聯定義為方法也能讓方法得以串連使用並進行查詢。舉例來說,我們可以在這個 posts
關聯中串上額外的查詢條件:
1$user->posts()->where('active', 1)->get();
1$user->posts()->where('active', 1)->get();
不過,在更深入瞭解如何使用關聯以前,我們先來了解一下如何定義 Eloquent 所支援的各種關聯型別吧!
一對一
一對一關聯是一種非常基本的資料庫關聯。舉例來說,一個 User
Model 可能與一個 Phone
Model 有關。要定義這個關聯,我們先在 User
Model 中定義一個 phone
方法。phone
方法應呼叫 hasOne
方法並回傳其結果。hasOne
方法是通過 Model 的 Illuminate\Database\Eloquent\Model
基礎類別提供的:
1<?php23namespace App\Models;45use Illuminate\Database\Eloquent\Model;67class User extends Model8{9 /**10 * Get the phone associated with the user.11 */12 public function phone()13 {14 return $this->hasOne(Phone::class);15 }16}
1<?php23namespace App\Models;45use Illuminate\Database\Eloquent\Model;67class User extends Model8{9 /**10 * Get the phone associated with the user.11 */12 public function phone()13 {14 return $this->hasOne(Phone::class);15 }16}
傳給 hasOne
方法的第一個引述是關聯 Model 類別的名稱。定義好關聯後,我們就可以通過 Eloquent 的動態屬性來存取這個關聯的紀錄。動態屬性能讓我們像在存取定義在 Model 上的屬性一樣來存取關聯方法:
1$phone = User::find(1)->phone;
1$phone = User::find(1)->phone;
Eloquent 會通過上層 Model 的名稱來判斷關聯的外部索引鍵 (Foreign Key)。在這個例子中,Eloquent 會自動假設 Phone
Model 中有個 user_id
外部索引鍵。若要複寫這個慣例用法的話,可以傳入第二個引數給 hasOne
方法:
1return $this->hasOne(Phone::class, 'foreign_key');
1return $this->hasOne(Phone::class, 'foreign_key');
此外,Eloquent 還會假設這個外部索引鍵應該要有個與上層資料的主索引鍵欄位相同的值。換句話說,Eloquent 會在 Phone
紀錄的 user_id
欄位中找到與該使用者 id
欄位值相同的資料。若想在關聯中使用 id
或 Model 的 $primaryKey
屬性意外的其他主索引鍵值的話,可傳入第三個引數給 hasOne
方法:
1return $this->hasOne(Phone::class, 'foreign_key', 'local_key');
1return $this->hasOne(Phone::class, 'foreign_key', 'local_key');
定義反向的關聯
好了,我們現在可以在 User
Model 中存取 Phone
Model 了。接著,我們來在 Phone
Model 上定義關聯,好讓我們能在存取擁有這隻電話的使用者。我們可以使用 belongsTo
方法來定義反向的 hasOne
關聯:
1<?php23namespace App\Models;45use Illuminate\Database\Eloquent\Model;67class Phone extends Model8{9 /**10 * Get the user that owns the phone.11 */12 public function user()13 {14 return $this->belongsTo(User::class);15 }16}
1<?php23namespace App\Models;45use Illuminate\Database\Eloquent\Model;67class Phone extends Model8{9 /**10 * Get the user that owns the phone.11 */12 public function user()13 {14 return $this->belongsTo(User::class);15 }16}
當叫用 user
方法時,Eloquent 會嘗試尋找一筆 id
符合 Phone
Model 中 user_id
欄位的 User
Model。
Eloquent 會檢查關聯方法的名稱,並在這個方法的名稱後加上 _id
來自動判斷外部索引鍵名稱。因此,在這個例子中,Eloquent 會假設 Phone
Model 有個 user_id
欄位。不過,若 Phone
Model 的外部索引鍵不是 user_id
,則可以傳遞一個自訂索引鍵名稱給 belongsTo
,作為第二個引數:
1/**2 * Get the user that owns the phone.3 */4public function user()5{6 return $this->belongsTo(User::class, 'foreign_key');7}
1/**2 * Get the user that owns the phone.3 */4public function user()5{6 return $this->belongsTo(User::class, 'foreign_key');7}
若上層 Model 不使用 id
作為其主索引鍵,或是想要使用不同的欄位來尋找關聯的 Model,則可以傳遞第三個引數給 belongsTo
方法來指定上層資料表的自訂索引鍵:
1/**2 * Get the user that owns the phone.3 */4public function user()5{6 return $this->belongsTo(User::class, 'foreign_key', 'owner_key');7}
1/**2 * Get the user that owns the phone.3 */4public function user()5{6 return $this->belongsTo(User::class, 'foreign_key', 'owner_key');7}
一對多
一對多關聯可用來定義某個有一個或多個子 Model 的單一 Model。舉例來說,部落格文章可能有無限數量筆留言。與其他 Eloquent 關聯一樣,一對多關聯可通過在 Eloquent Model 中定義方法來定義:
1<?php23namespace App\Models;45use Illuminate\Database\Eloquent\Model;67class Post extends Model8{9 /**10 * Get the comments for the blog post.11 */12 public function comments()13 {14 return $this->hasMany(Comment::class);15 }16}
1<?php23namespace App\Models;45use Illuminate\Database\Eloquent\Model;67class Post extends Model8{9 /**10 * Get the comments for the blog post.11 */12 public function comments()13 {14 return $this->hasMany(Comment::class);15 }16}
請記得,Eloquent 會自動為 Comment
Model 判斷適當的外部索引鍵欄位。依照慣例,Eloquent 會去上層 Model 的「蛇形命名法 (snake_case)」名稱,並在其後加上 _id
。因此,在這個例子中,Eloquent 會假設 Comment
Model 上的外部索引鍵欄位為 post_id
。
定義好關聯方法後,我們就可以通過 comments
屬性來存取關聯留言的 Collection。請記得,由於 Eloquent 提供了「動態關聯屬性」,因此我們可以像我們是在 Model 上定義屬性一樣地存取關聯方法:
1use App\Models\Post;23$comments = Post::find(1)->comments;45foreach ($comments as $comment) {6 //7}
1use App\Models\Post;23$comments = Post::find(1)->comments;45foreach ($comments as $comment) {6 //7}
由於所有的關聯也同時是 Query Builder,因此我們也能通過呼叫 comments
方法並繼續在查詢上串上條件來進一步給關聯加上查詢條件:
1$comment = Post::find(1)->comments()2 ->where('title', 'foo')3 ->first();
1$comment = Post::find(1)->comments()2 ->where('title', 'foo')3 ->first();
就像 hasOne
方法,我們也可以通過傳遞額外的參數給 hasMany
來複寫外部與內部的索引鍵:
1return $this->hasMany(Comment::class, 'foreign_key');23return $this->hasMany(Comment::class, 'foreign_key', 'local_key');
1return $this->hasMany(Comment::class, 'foreign_key');23return $this->hasMany(Comment::class, 'foreign_key', 'local_key');
一對多 (反向) / 隸屬於 (Belongs To)
現在,我們已經可以存取一篇文章的所有留言了。讓我們來定義一個關聯,以從留言去的其上層的文章。要定義 hasMany
關聯的相反,我們可以在子 Model 中定義一個呼叫了 belongsTo
方法的關聯方法:
1<?php23namespace App\Models;45use Illuminate\Database\Eloquent\Model;67class Comment extends Model8{9 /**10 * Get the post that owns the comment.11 */12 public function post()13 {14 return $this->belongsTo(Post::class);15 }16}
1<?php23namespace App\Models;45use Illuminate\Database\Eloquent\Model;67class Comment extends Model8{9 /**10 * Get the post that owns the comment.11 */12 public function post()13 {14 return $this->belongsTo(Post::class);15 }16}
定義好關聯後,我們就可以通過存取 post
「動態關聯屬性」來取得留言的上層文章:
1use App\Models\Comment;23$comment = Comment::find(1);45return $comment->post->title;
1use App\Models\Comment;23$comment = Comment::find(1);45return $comment->post->title;
在上述例子中,Eloquent 會嘗試找到 id
符合 Comments
Model 中 post_id
欄位的 Post
Model。
Eloquent 會檢查關聯方法的名稱,並在該名稱後加上 _
,然後再加上上層 Model 的主索引鍵欄位名稱作為預設的外部索引鍵名稱。因此,在這個例子中,Eloquent 會假設 Post
Model 在 comments
資料表中的外部索引鍵為 post_id
。
不過,若沒有依照這種慣例來命名關聯的外部索引鍵,則可以將自訂的外部索引鍵傳遞給 belongsTo
方法作為第二個引數:
1/**2 * Get the post that owns the comment.3 */4public function post()5{6 return $this->belongsTo(Post::class, 'foreign_key');7}
1/**2 * Get the post that owns the comment.3 */4public function post()5{6 return $this->belongsTo(Post::class, 'foreign_key');7}
若上層 Model 不使用 id
作為其主索引鍵,或是想要使用不同的欄位來尋找關聯的 Model,則可以傳遞第三個引數給 belongsTo
方法來指定上層資料表的自訂索引鍵:
1/**2 * Get the post that owns the comment.3 */4public function post()5{6 return $this->belongsTo(Post::class, 'foreign_key', 'owner_key');7}
1/**2 * Get the post that owns the comment.3 */4public function post()5{6 return $this->belongsTo(Post::class, 'foreign_key', 'owner_key');7}
預設 Model
belongsTo
, hasOne
, hasOneThrough
, 以及 morphOne
關聯可定義一個預設 Model,當給定的關聯為 null
時會回傳該預設 Model。這種模式通常稱為 Null Object pattern,並能讓你在程式碼中減少條件檢查的次數。在下列範例中,user
關聯會在沒有使用者附加在 Post
Model 時回傳一個空的 App\Models\User
Model:
1/**2 * Get the author of the post.3 */4public function user()5{6 return $this->belongsTo(User::class)->withDefault();7}
1/**2 * Get the author of the post.3 */4public function user()5{6 return $this->belongsTo(User::class)->withDefault();7}
若要為預設的 Model 設定屬性,則可以傳入陣列或閉包給 withDefault
方法:
1/**2 * Get the author of the post.3 */4public function user()5{6 return $this->belongsTo(User::class)->withDefault([7 'name' => 'Guest Author',8 ]);9}1011/**12 * Get the author of the post.13 */14public function user()15{16 return $this->belongsTo(User::class)->withDefault(function ($user, $post) {17 $user->name = 'Guest Author';18 });19}
1/**2 * Get the author of the post.3 */4public function user()5{6 return $this->belongsTo(User::class)->withDefault([7 'name' => 'Guest Author',8 ]);9}1011/**12 * Get the author of the post.13 */14public function user()15{16 return $this->belongsTo(User::class)->withDefault(function ($user, $post) {17 $user->name = 'Guest Author';18 });19}
查詢 Belongs To 關聯
在查詢「Belongs To」關聯的子項目時,可以手動建立用於取得相應 Eloquent Model 的 where
子句:
1use App\Models\Post;23$posts = Post::where('user_id', $user->id)->get();
1use App\Models\Post;23$posts = Post::where('user_id', $user->id)->get();
不過,使用 whereBelongsTo
方法可能會比較方便。該方法會為給定的 Model 自動判斷適當的關聯與外部索引鍵:
1$posts = Post::whereBelongsTo($user)->get();
1$posts = Post::whereBelongsTo($user)->get();
預設情況下,Larave 會依據 Model 的類別名稱來判斷與給定 Model 有關的關聯。不過,我們也可以通過傳入第二個引數給 whereBelongsTo
方法來手動指定關聯的名稱:
1$posts = Post::whereBelongsTo($user, 'author')->get();
1$posts = Post::whereBelongsTo($user, 'author')->get();
一對多中之一
有時候,某個 Model 可能有多個關聯 Model,而我們可能會想取多個關聯 Model 中「最新」或「最舊」的關聯 Model。舉例來說,User
Model (使用者) 可能會關聯到多個 Order
Model (訂單),而我們可能會想定義一種方便的方法來存取使用者最新的訂單。我們可以通過將 hasOne
關聯類型與 ofMany
方法搭配使用來達成:
1/**2 * Get the user's most recent order.3 */4public function latestOrder()5{6 return $this->hasOne(Order::class)->latestOfMany();7}
1/**2 * Get the user's most recent order.3 */4public function latestOrder()5{6 return $this->hasOne(Order::class)->latestOfMany();7}
同樣的,我們也可以定義一個方法來取得一個關聯中「最舊」或第一個關聯的 Model:
1/**2 * Get the user's oldest order.3 */4public function oldestOrder()5{6 return $this->hasOne(Order::class)->oldestOfMany();7}
1/**2 * Get the user's oldest order.3 */4public function oldestOrder()5{6 return $this->hasOne(Order::class)->oldestOfMany();7}
預設情況下,latestOfMany
與 oldestOfMany
方法會依照該 Model 的主索引鍵來取得最新或最舊的 Model,而該索引鍵必須要是可以排序的。不過,有時候我們可能會想從一個更大的關聯中通過另一種方法來取得單一 Model:
舉例來說,我們可以使用 ofMany
方法來去的使用者下過金額最高的訂單。ofMany
方法的第一個引數為可排序的欄位,接著則是要套用哪個匯總函式 (min
或 max
等) 在關聯的 Model 上:
1/**2 * Get the user's largest order.3 */4public function largestOrder()5{6 return $this->hasOne(Order::class)->ofMany('price', 'max');7}
1/**2 * Get the user's largest order.3 */4public function largestOrder()5{6 return $this->hasOne(Order::class)->ofMany('price', 'max');7}
由於 PostgreSQL 不支援在 UUID 欄位上執行 MAX
函式,因此目前一對多關聯無法搭配 PostgreSQL 的 UUID 欄位使用。
進階的一對多中之一關聯
我們還可以進一步地做出進階的「一對多中之一」關聯。舉例來說,Product
Model 可能會有許多相應的 Price
Model,這些 Price
Model 會在每次更新商品價格後保留在系統內。此外,我們也可以進一步地通過 published_at
欄位來讓某個商品價格在未來的時間點生效。
因此,總結一下,我們會需要取得最新且已發布的價格,且發佈時間不可是未來。此外,若有兩個價格的發佈時間相同,則我們取 ID 最大的那個價格。為此,我們必須傳入一個陣列給 ofMany
方法,該陣列序包用來判斷最新價格的可排序欄位。此外,我們會提供一個閉包給 ofMany
方法作為第二個引述。這個閉包會負責為關聯查詢加上額外的發佈時間條件:
1/**2 * Get the current pricing for the product.3 */4public function currentPricing()5{6 return $this->hasOne(Price::class)->ofMany([7 'published_at' => 'max',8 'id' => 'max',9 ], function ($query) {10 $query->where('published_at', '<', now());11 });12}
1/**2 * Get the current pricing for the product.3 */4public function currentPricing()5{6 return $this->hasOne(Price::class)->ofMany([7 'published_at' => 'max',8 'id' => 'max',9 ], function ($query) {10 $query->where('published_at', '<', now());11 });12}
間接一對一
「間接一對一 (has-one-through)」關聯定義了與另一個 Model 間的一對一關係。不過,使用這種關聯代表宣告關聯的 Model 可以 通過 一個 Model 來對應到另一個 Model 的實體。
舉例來說,在汽車維修網站中,每個 Mechanic
Model (零件) 可以跟一個 Car
Model 關聯。而每個 Car
Model (汽車) 則可以關聯到一個 Owner
Model (車主)。雖然零件與車主在資料庫中並沒有直接的關聯性,但我們可以 通過 Car
Model 來在零件上存取車主。來看看要定義這種關聯所需的資料表:
1mechanics2 id - integer3 name - string45cars6 id - integer7 model - string8 mechanic_id - integer910owners11 id - integer12 name - string13 car_id - integer
1mechanics2 id - integer3 name - string45cars6 id - integer7 model - string8 mechanic_id - integer910owners11 id - integer12 name - string13 car_id - integer
現在,我們已經瞭解了這種關聯性的資料表結構。讓我們來在 Mechanic
Model 上定義關聯:
1<?php23namespace App\Models;45use Illuminate\Database\Eloquent\Model;67class Mechanic extends Model8{9 /**10 * Get the car's owner.11 */12 public function carOwner()13 {14 return $this->hasOneThrough(Owner::class, Car::class);15 }16}
1<?php23namespace App\Models;45use Illuminate\Database\Eloquent\Model;67class Mechanic extends Model8{9 /**10 * Get the car's owner.11 */12 public function carOwner()13 {14 return $this->hasOneThrough(Owner::class, Car::class);15 }16}
傳給 hasOneThrough
方法的第一個引述是最後我們想存取的 Model 名稱;第二個引數則是中介 Model 的名稱。
索引鍵慣例
在進行關聯查詢時,會使用到典型的 Eloquent 外部索引鍵慣例。若想自訂關聯使用的索引鍵,則可以將自訂索引鍵傳給 hasOneThrough
方法的第三個與第四個引數。第三個引數為中介 Model 上的外部索引鍵名稱。第四個引數則是最終 Model 的外部索引鍵名稱。第五個引數則為內部索引鍵,而第六個引述則是中介 Model 上的內部索引鍵:
1class Mechanic extends Model2{3 /**4 * Get the car's owner.5 */6 public function carOwner()7 {8 return $this->hasOneThrough(9 Owner::class,10 Car::class,11 'mechanic_id', // cars 表上的外部索引鍵...12 'car_id', // owners 表上的外部索引鍵...13 'id', // mechanics 表上的內部索引鍵...14 'id' // cars 表上的內部索引鍵...15 );16 }17}
1class Mechanic extends Model2{3 /**4 * Get the car's owner.5 */6 public function carOwner()7 {8 return $this->hasOneThrough(9 Owner::class,10 Car::class,11 'mechanic_id', // cars 表上的外部索引鍵...12 'car_id', // owners 表上的外部索引鍵...13 'id', // mechanics 表上的內部索引鍵...14 'id' // cars 表上的內部索引鍵...15 );16 }17}
間接一對多
「間接一對多 (has-many-through)」關聯提供了一個方便的方法來通過中介關聯存取另一個關聯。舉例來說,假設我們有一個像 Laravel Vapor 這樣的部署平台。Project
Model (專案)可通過一個中介的 Environment
Model (環境) 來存取多個 Deployment
Model (部署)。依照這個例子,我們可以很輕鬆的取得特定專案的所有部署。來看看定義這個關聯性所需的資料表:
1projects2 id - integer3 name - string45environments6 id - integer7 project_id - integer8 name - string910deployments11 id - integer12 environment_id - integer13 commit_hash - string
1projects2 id - integer3 name - string45environments6 id - integer7 project_id - integer8 name - string910deployments11 id - integer12 environment_id - integer13 commit_hash - string
現在,我們已經瞭解了這種關聯性的資料表結構。讓我們來在 Project
Model 上定義關聯:
1<?php23namespace App\Models;45use Illuminate\Database\Eloquent\Model;67class Project extends Model8{9 /**10 * Get all of the deployments for the project.11 */12 public function deployments()13 {14 return $this->hasManyThrough(Deployment::class, Environment::class);15 }16}
1<?php23namespace App\Models;45use Illuminate\Database\Eloquent\Model;67class Project extends Model8{9 /**10 * Get all of the deployments for the project.11 */12 public function deployments()13 {14 return $this->hasManyThrough(Deployment::class, Environment::class);15 }16}
傳給 hasManyThrough
方法的第一個引述是最後我們想存取的 Model 名稱;第二個引數則是中介 Model 的名稱。
雖然 Deployment
Model 的資料表不包含 project_id
欄位,但 hasManyThrough
關聯可讓我們通過 $project->deployments
來存取專案的部署。為了取得這些 Model,Eloquent 會先在中介的 Environment
Model 資料表上讀取 project_id
。找到相關的環境 ID 後,再通過這些 ID 來查詢 Deployment
Model 的資料表。
索引鍵慣例
在進行關聯查詢時,會使用到典型的 Eloquent 外部索引鍵慣例。若想自訂關聯使用的索引鍵,則可以將自訂索引鍵傳給 hasManyThrough
方法的第三個與第四個引數。第三個引數為中介 Model 上的外部索引鍵名稱。第四個引數則是最終 Model 的外部索引鍵名稱。第五個引數則為內部索引鍵,而第六個引述則是中介 Model 上的內部索引鍵:
1class Project extends Model2{3 public function deployments()4 {5 return $this->hasManyThrough(6 Deployment::class,7 Environment::class,8 'project_id', // environments 表上的外部索引鍵...9 'environment_id', // deployments 表上的外部索引鍵...10 'id', // projects 表上的內部索引鍵...11 'id' // environments 表上的內部索引鍵...12 );13 }14}
1class Project extends Model2{3 public function deployments()4 {5 return $this->hasManyThrough(6 Deployment::class,7 Environment::class,8 'project_id', // environments 表上的外部索引鍵...9 'environment_id', // deployments 表上的外部索引鍵...10 'id', // projects 表上的內部索引鍵...11 'id' // environments 表上的內部索引鍵...12 );13 }14}
Many To Many 關聯
比起 hasOne
或 hasMany
,多對多關聯稍微複雜一點。一個多對多關聯的例子是:一位使用者可以有多個職位,而這些職位也會被網站中的其他使用者使用。舉例來說,某位使用者可能會被設定職位「作者」與「編輯」,但這些職位也可能會被指派給其他使用者。因此,一位使用者可以有多個職位,而一個職位則可以有多位使用者。
資料表結構
要定義這種關聯,我們需要三張資料表:users
, roles
, 與 role_user
。role_user
資料表的名稱是由關聯的 Model 名稱按照字母排序串接而來的,裡面包含了 user_id
與 role_id
欄位。這張資料表會用來作為關聯使用者與職位的中介資料表。
請記得,由於一個職位可以同時關聯到多位使用者,因此我們沒辦法在 roles
資料表上設定 user_id
欄位。若這麼做的話,一個職位就只能有一位使用者。為了要讓職位能被設定給多位使用者,我們會需要 role_user
資料表。我們可以總結一下,資料表的結構會長這樣:
1users2 id - integer3 name - string45roles6 id - integer7 name - string89role_user10 user_id - integer11 role_id - integer
1users2 id - integer3 name - string45roles6 id - integer7 name - string89role_user10 user_id - integer11 role_id - integer
Model 架構
我們可以通過撰寫一個回傳 belongsToMany
方法執行結果的方法來定義多對多關聯。belongsToMany
方法是由 Illuminate\Database\Eloquent\Model
基礎類別提供的,你的專案中所有的 Eloquent Model 都使用了這個類別。舉例來說,讓我們來在 User
Model 上定義一個 roles
方法。傳入這個方法的第一個引述是關聯 Model 類別的名稱:
1<?php23namespace App\Models;45use Illuminate\Database\Eloquent\Model;67class User extends Model8{9 /**10 * The roles that belong to the user.11 */12 public function roles()13 {14 return $this->belongsToMany(Role::class);15 }16}
1<?php23namespace App\Models;45use Illuminate\Database\Eloquent\Model;67class User extends Model8{9 /**10 * The roles that belong to the user.11 */12 public function roles()13 {14 return $this->belongsToMany(Role::class);15 }16}
定義好關聯後,就可以使用 roles
動態關聯屬性來存取該使用者的角色:
1use App\Models\User;23$user = User::find(1);45foreach ($user->roles as $role) {6 //7}
1use App\Models\User;23$user = User::find(1);45foreach ($user->roles as $role) {6 //7}
由於所有的關聯也同時是 Query Builder,因此我們也能通過呼叫 roles
方法並繼續在查詢上串上條件來進一步給關聯加上查詢條件:
1$roles = User::find(1)->roles()->orderBy('name')->get();
1$roles = User::find(1)->roles()->orderBy('name')->get();
為了判斷該關聯的中介資料表表名,Eloquent 會將兩個關聯 Model 的名稱按照字母排序串接在一起。不過,這個慣例是可以隨意複寫的,只需要傳入第二個引數給 belongsToMany
方法即可:
1return $this->belongsToMany(Role::class, 'role_user');
1return $this->belongsToMany(Role::class, 'role_user');
除了自訂中介表的表名外,也可以傳入額外的引數給 belongsToMany
來自訂中介表上的欄位名稱。第三個引數目前定義關聯的 Model 的外部索引鍵,而第四個引述則是要連結的 Model 的外部索引鍵:
1return $this->belongsToMany(Role::class, 'role_user', 'user_id', 'role_id');
1return $this->belongsToMany(Role::class, 'role_user', 'user_id', 'role_id');
定義反向的關聯
若要定義 many-to-many 的「相反」關聯,應先在關聯的 Model 上定義一個同樣回傳 belongsToMany
方法結果的方法。接著我們的使用者與角色的例子,我們來在 Role
Model 上定義 users
方法:
1<?php23namespace App\Models;45use Illuminate\Database\Eloquent\Model;67class Role extends Model8{9 /**10 * The users that belong to the role.11 */12 public function users()13 {14 return $this->belongsToMany(User::class);15 }16}
1<?php23namespace App\Models;45use Illuminate\Database\Eloquent\Model;67class Role extends Model8{9 /**10 * The users that belong to the role.11 */12 public function users()13 {14 return $this->belongsToMany(User::class);15 }16}
如你所見,除了這邊是參照 App\Models\User
外,關聯定義跟 User
Model 中對應的部分完全一樣。由於我們使用的還是 belongsToMany
方法,因此,在定義「反向」的 many-to-many 關聯時,一樣可以使用一般的資料表與索引鍵自訂選項。
取得中介資料表欄位
讀者可能已經瞭解到,處理 Many-to-Many 關聯時必須要有一張中介資料表。Eloquent 提供了一些非常適用的方法來與中介資料表互動。舉例來說,假設 User
Model 有許多關聯的 Role
Model。存取這個關聯後,我們可以使用 Model 上的 pivot
屬性來存取中介資料表:
1use App\Models\User;23$user = User::find(1);45foreach ($user->roles as $role) {6 echo $role->pivot->created_at;7}
1use App\Models\User;23$user = User::find(1);45foreach ($user->roles as $role) {6 echo $role->pivot->created_at;7}
可以注意到,我們取得的每個 Role
資料表都會自動獲得一個 pivot
屬性。這個屬性包含了一個代表中介資料表的 Model。
預設情況下,只有 Model 的索引鍵會出現在 Pivot
Model 上。若中介資料表包含了其他額外的屬性,則需要在定義關聯時指定這些屬性:
1return $this->belongsToMany(Role::class)->withPivot('active', 'created_by');
1return $this->belongsToMany(Role::class)->withPivot('active', 'created_by');
若想讓中介資料表擁有 Eloquent 能自動維護的 created_at
與 updated_at
時戳,可在定義關聯的時候呼叫 withTimestamps
方法:
1return $this->belongsToMany(Role::class)->withTimestamps();
1return $this->belongsToMany(Role::class)->withTimestamps();
使用 Eloquent 自動維護時戳的中介資料表會需要擁有 created_at
與 updated_at
兩個時戳欄位。
自訂 pivot
屬性名稱
剛才也有提過,我們可以使用 pivot
屬性來存取中介資料表的屬性。不過,我們可以自訂這個屬性的名稱以讓其跟貼合在專案中的用途。
舉例來說,我們的專案中可能會包含能讓使用者訂閱 Podcast 的功能,我們可能會想在使用者與 Podcast 間使用 Many-to-Many 關聯。在這個例子中,我們可能會想將中介資料表屬性的名稱從 pivot
改成 subscription
。可以在定義關聯時使用 as
方法來完成:
1return $this->belongsToMany(Podcast::class)2 ->as('subscription')3 ->withTimestamps();
1return $this->belongsToMany(Podcast::class)2 ->as('subscription')3 ->withTimestamps();
指定好自訂的中介資料表屬性後,就可以使用自訂的名稱來存取中介資料表資料:
1$users = User::with('podcasts')->get();23foreach ($users->flatMap->podcasts as $podcast) {4 echo $podcast->subscription->created_at;5}
1$users = User::with('podcasts')->get();23foreach ($users->flatMap->podcasts as $podcast) {4 echo $podcast->subscription->created_at;5}
通過中介資料表欄位來過濾查詢
也可以在定義關聯時使用 wherePivot
, wherePivotIn
, wherePivotNotIn
, wherePivotBetween
, wherePivotNotBetween
, wherePivotNull
, 與 wherePivotNotNull
方法來過濾 belongsToMany
關聯查詢的回傳結果:
1return $this->belongsToMany(Role::class)2 ->wherePivot('approved', 1);34return $this->belongsToMany(Role::class)5 ->wherePivotIn('priority', [1, 2]);67return $this->belongsToMany(Role::class)8 ->wherePivotNotIn('priority', [1, 2]);910return $this->belongsToMany(Podcast::class)11 ->as('subscriptions')12 ->wherePivotBetween('created_at', ['2020-01-01 00:00:00', '2020-12-31 00:00:00']);1314return $this->belongsToMany(Podcast::class)15 ->as('subscriptions')16 ->wherePivotNotBetween('created_at', ['2020-01-01 00:00:00', '2020-12-31 00:00:00']);1718return $this->belongsToMany(Podcast::class)19 ->as('subscriptions')20 ->wherePivotNull('expired_at');2122return $this->belongsToMany(Podcast::class)23 ->as('subscriptions')24 ->wherePivotNotNull('expired_at');
1return $this->belongsToMany(Role::class)2 ->wherePivot('approved', 1);34return $this->belongsToMany(Role::class)5 ->wherePivotIn('priority', [1, 2]);67return $this->belongsToMany(Role::class)8 ->wherePivotNotIn('priority', [1, 2]);910return $this->belongsToMany(Podcast::class)11 ->as('subscriptions')12 ->wherePivotBetween('created_at', ['2020-01-01 00:00:00', '2020-12-31 00:00:00']);1314return $this->belongsToMany(Podcast::class)15 ->as('subscriptions')16 ->wherePivotNotBetween('created_at', ['2020-01-01 00:00:00', '2020-12-31 00:00:00']);1718return $this->belongsToMany(Podcast::class)19 ->as('subscriptions')20 ->wherePivotNull('expired_at');2122return $this->belongsToMany(Podcast::class)23 ->as('subscriptions')24 ->wherePivotNotNull('expired_at');
定義自訂的中介表 Model
若想定義一個代表多對多關聯之中介資料表的自訂 Model,則可以在定義關聯時呼叫 using
方法。自訂樞紐 Model (Pivot Model) 能讓我們有機會在樞紐 Model 上定義一些額外的方法。
要自訂多對多樞紐 Model,則應繼承 Illuminate\Database\Eloquent\Relations\Pivot
類別。多型多對多的樞紐 Model 則應繼承 Illuminate\Database\Eloquent\Relations\MorphPivot
。舉例來說,我們可以定義一個使用了 RoleUser
樞紐 Model 的 Role
Model:
1<?php23namespace App\Models;45use Illuminate\Database\Eloquent\Model;67class Role extends Model8{9 /**10 * The users that belong to the role.11 */12 public function users()13 {14 return $this->belongsToMany(User::class)->using(RoleUser::class);15 }16}
1<?php23namespace App\Models;45use Illuminate\Database\Eloquent\Model;67class Role extends Model8{9 /**10 * The users that belong to the role.11 */12 public function users()13 {14 return $this->belongsToMany(User::class)->using(RoleUser::class);15 }16}
定義 RoleUser
Model 時,應繼承 Illuminate\Database\Eloquent\Relations\Pivot
類別:
1<?php23namespace App\Models;45use Illuminate\Database\Eloquent\Relations\Pivot;67class RoleUser extends Pivot8{9 //10}
1<?php23namespace App\Models;45use Illuminate\Database\Eloquent\Relations\Pivot;67class RoleUser extends Pivot8{9 //10}
樞紐 Model 不能使用 SoftDeletes
Trait。若有需要對樞紐紀錄作軟刪除,請考慮將樞紐 Model 改寫成真正的 Eloquent Model。
自訂樞紐 Model 並遞增 ID
若有定義了使用自訂樞紐 Model 的多對多關聯,且該樞紐 Model 由自動遞增的主索引鍵 (Auto-Incrementing Primary Key),則應確保這個自訂樞紐 Model 類別由定義一個設為 true
的 incrementing
屬性。
1/**2 * Indicates if the IDs are auto-incrementing.3 *4 * @var bool5 */6public $incrementing = true;
1/**2 * Indicates if the IDs are auto-incrementing.3 *4 * @var bool5 */6public $incrementing = true;
Polymorphic (多型) 關聯
使用多型關聯,就能讓子 Model 通過單一關聯來隸屬於多種 Model。舉例來說,假設我們正在製作一個能讓使用者分享部落格貼文與影片的網站。在這種例子中,Comment
(留言) Model 有可能隸屬於 Post
(貼文) Model,也可能隸屬於 Video
(影片) Model。
一對一 (多型)
資料表結構
多型的一對一關聯於一般的一對一關聯類似。不過,在這種關聯中的子 Model 可以使用一種關聯來表示出對超過一種 Model 的從屬關係。舉例來說,部落格的 Post
(貼文) 與 User
(使用者) 可能會共享一個多型關聯的 Image
(圖片) Model。使用多型的一對一關聯,就能讓我們製作一張用來儲存不重複圖片的資料表,並將該資料表關聯到貼文跟使用者上。首先,我們來看看下列資料表架構:
1posts2 id - integer3 name - string45users6 id - integer7 name - string89images10 id - integer11 url - string12 imageable_id - integer13 imageable_type - string
1posts2 id - integer3 name - string45users6 id - integer7 name - string89images10 id - integer11 url - string12 imageable_id - integer13 imageable_type - string
可以注意到 images
資料表上的 imageable_id
與 imageable_type
欄位。imageable_id
欄位用來包含貼文或使用者的 ID 值,而 imageable_type
欄位則用來包含上層 Model 的類別名稱。imageable_type
是用來給 Eloquent 判斷上層 Model 的「型別 (Type)」,以在存取 imageable
關聯時能回傳該上層 Model。在這種情況下,這個欄位的內容會是 App\Models\Post
或 App\Models\User
。
Model 架構
接著,讓我們來看看要製作這種關聯所需要的 Model 定義:
1<?php23namespace App\Models;45use Illuminate\Database\Eloquent\Model;67class Image extends Model8{9 /**10 * Get the parent imageable model (user or post).11 */12 public function imageable()13 {14 return $this->morphTo();15 }16}1718class Post extends Model19{20 /**21 * Get the post's image.22 */23 public function image()24 {25 return $this->morphOne(Image::class, 'imageable');26 }27}2829class User extends Model30{31 /**32 * Get the user's image.33 */34 public function image()35 {36 return $this->morphOne(Image::class, 'imageable');37 }38}
1<?php23namespace App\Models;45use Illuminate\Database\Eloquent\Model;67class Image extends Model8{9 /**10 * Get the parent imageable model (user or post).11 */12 public function imageable()13 {14 return $this->morphTo();15 }16}1718class Post extends Model19{20 /**21 * Get the post's image.22 */23 public function image()24 {25 return $this->morphOne(Image::class, 'imageable');26 }27}2829class User extends Model30{31 /**32 * Get the user's image.33 */34 public function image()35 {36 return $this->morphOne(Image::class, 'imageable');37 }38}
取得關聯
定義好資料庫資料表與 Model 後,就可以通過這些 Model 來存取關聯。舉例來說,若要取得一則貼文的圖片,我們可以存取 image
動態關聯屬性:
1use App\Models\Post;23$post = Post::find(1);45$image = $post->image;
1use App\Models\Post;23$post = Post::find(1);45$image = $post->image;
可以通過存取呼叫 morphTo
之方法的名稱來取得多型 Model 的上層 Model。在這個例子中,就是 Image
Model 的 imageable
方法。因此,我們可以用動態關聯屬性來存取該方法:
1use App\Models\Image;23$image = Image::find(1);45$imageable = $image->imageable;
1use App\Models\Image;23$image = Image::find(1);45$imageable = $image->imageable;
依據擁有該圖片的 Model 類型,Image
Model 上的 imageable
關聯會回傳 Post
或 User
實體。
索引鍵慣例
若有需要,也可以指定多型子 Model 所使用的「id」與「type」欄位名稱。若要自訂這些欄位的名稱,請先確保有將關聯的名稱傳給 morphTo
方法的第一個引數。一般來說,這個值應該要與方法名稱相同,因此我們可以使用 PHP 的 __FUNCTION__
常數:
1/**2 * Get the model that the image belongs to.3 */4public function imageable()5{6 return $this->morphTo(__FUNCTION__, 'imageable_type', 'imageable_id');7}
1/**2 * Get the model that the image belongs to.3 */4public function imageable()5{6 return $this->morphTo(__FUNCTION__, 'imageable_type', 'imageable_id');7}
一對多 (多型)
資料表結構
One-to-Many 的多型關聯與一般的 One-to-Many 關聯很類似。不過,在多型關聯中,可以使用單一關聯來讓子 Model 可以隸屬於多種類型的 Model。舉例來說,假設有個使用者可以在貼文與影片上「留言」的網站。若使用多型關聯,我們可以使用單一一個 comments
表來包含用於貼文與影片的留言。首先,來看看需要建立這種關聯的資料表結構:
1posts2 id - integer3 title - string4 body - text56videos7 id - integer8 title - string9 url - string1011comments12 id - integer13 body - text14 commentable_id - integer15 commentable_type - string
1posts2 id - integer3 title - string4 body - text56videos7 id - integer8 title - string9 url - string1011comments12 id - integer13 body - text14 commentable_id - integer15 commentable_type - string
Model 架構
接著,讓我們來看看要製作這種關聯所需要的 Model 定義:
1<?php23namespace App\Models;45use Illuminate\Database\Eloquent\Model;67class Comment extends Model8{9 /**10 * Get the parent commentable model (post or video).11 */12 public function commentable()13 {14 return $this->morphTo();15 }16}1718class Post extends Model19{20 /**21 * Get all of the post's comments.22 */23 public function comments()24 {25 return $this->morphMany(Comment::class, 'commentable');26 }27}2829class Video extends Model30{31 /**32 * Get all of the video's comments.33 */34 public function comments()35 {36 return $this->morphMany(Comment::class, 'commentable');37 }38}
1<?php23namespace App\Models;45use Illuminate\Database\Eloquent\Model;67class Comment extends Model8{9 /**10 * Get the parent commentable model (post or video).11 */12 public function commentable()13 {14 return $this->morphTo();15 }16}1718class Post extends Model19{20 /**21 * Get all of the post's comments.22 */23 public function comments()24 {25 return $this->morphMany(Comment::class, 'commentable');26 }27}2829class Video extends Model30{31 /**32 * Get all of the video's comments.33 */34 public function comments()35 {36 return $this->morphMany(Comment::class, 'commentable');37 }38}
取得關聯
定義好資料表與 Model 後,就可以使用 Model 的動態關聯屬性來存取這個關聯。舉例來說,若要存取某個貼文的所有留言,我們可以使用 comments
動態屬性:
1use App\Models\Post;23$post = Post::find(1);45foreach ($post->comments as $comment) {6 //7}
1use App\Models\Post;23$post = Post::find(1);45foreach ($post->comments as $comment) {6 //7}
也可以通過存取呼叫 morphTo
之方法的名稱來取得多型子 Model 的上層 Model。在這個例子中,就是 Comment
Model 的 commentable
方法。因此,我們可以用動態關聯屬性來存取該方法以取得留言的上層 Model:
1use App\Models\Comment;23$comment = Comment::find(1);45$commentable = $comment->commentable;
1use App\Models\Comment;23$comment = Comment::find(1);45$commentable = $comment->commentable;
依照不同的留言上層 Model 類型,Comment
Model 的 commentable
關聯回傳的不是 Post
實體就是 Video
實體。
多中之一 (多型)
有時候,某個 Model 可能有多個關聯 Model,而我們可能會想取多個關聯 Model 中「最新」或「最舊」的關聯 Model。舉例來說,User
Model (使用者) 可能會關聯到多個 Image
Model (圖片),而我們可能會想定義一種方便的方法來存取使用者最新的圖片。我們可以通過將 morphOne
關聯類型與 ofMany
方法搭配使用來達成:
1/**2 * Get the user's most recent image.3 */4public function latestImage()5{6 return $this->morphOne(Image::class, 'imageable')->latestOfMany();7}
1/**2 * Get the user's most recent image.3 */4public function latestImage()5{6 return $this->morphOne(Image::class, 'imageable')->latestOfMany();7}
同樣的,我們也可以定義一個方法來取得一個關聯中「最舊」或第一個關聯的 Model:
1/**2 * Get the user's oldest image.3 */4public function oldestImage()5{6 return $this->morphOne(Image::class, 'imageable')->oldestOfMany();7}
1/**2 * Get the user's oldest image.3 */4public function oldestImage()5{6 return $this->morphOne(Image::class, 'imageable')->oldestOfMany();7}
預設情況下,latestOfMany
與 oldestOfMany
方法會依照該 Model 的主索引鍵來取得最新或最舊的 Model,而該索引鍵必須要是可以排序的。不過,有時候我們可能會想從一個更大的關聯中通過另一種方法來取得單一 Model:
舉例來說,我們可以使用 ofMany
方法來去的使用者獲得最多「讚」的圖片。ofMany
方法的第一個引數為可排序的欄位,接著則是要套用哪個匯總函式 (min
或 max
等) 在關聯的 Model 上:
1/**2 * Get the user's most popular image.3 */4public function bestImage()5{6 return $this->morphOne(Image::class, 'imageable')->ofMany('likes', 'max');7}
1/**2 * Get the user's most popular image.3 */4public function bestImage()5{6 return $this->morphOne(Image::class, 'imageable')->ofMany('likes', 'max');7}
還有辦法建立建立更進階的「One of Many」關聯。更多資訊請參考 Has One of Many 說明文件。
多對多 (多型)
資料表結構
多型的 Many-to-Many 關聯比「Morph One」或「Morph Many」都稍微複雜一點。舉例來說,Post
Model 與 Video
Model 可以共用一個多型關聯的 Tag
Model。在這種情況下使用多型的 Many-to-Many 可以讓我們的專案中只需要一張資料表來儲存獨立的 Tag,就可以關聯給 Post 跟 Video。首先,來看看要建立這種關聯的資料表架構:
1posts2 id - integer3 name - string45videos6 id - integer7 name - string89tags10 id - integer11 name - string1213taggables14 tag_id - integer15 taggable_id - integer16 taggable_type - string
1posts2 id - integer3 name - string45videos6 id - integer7 name - string89tags10 id - integer11 name - string1213taggables14 tag_id - integer15 taggable_id - integer16 taggable_type - string
在進一步深入瞭解多型的 Many-to-Many 關聯前,我們建議你先閱讀有關普通 Many-to-Many 關聯的說明文件。
Model 架構
接著,我們就可以開始在 Model 上定義關聯了。Post
與 Video
Model 都包含了一個 tags
方法,該方法中會呼叫基礎 Eloquent Model 類別中的 morphToMany
方法。
morphToMany
方法接受關聯 Model 的名稱,以及「關聯名稱」。根據我們設定給中介表的名稱以及其中包含的索引鍵,我們可以將關聯推導為「taggable」:
1<?php23namespace App\Models;45use Illuminate\Database\Eloquent\Model;67class Post extends Model8{9 /**10 * Get all of the tags for the post.11 */12 public function tags()13 {14 return $this->morphToMany(Tag::class, 'taggable');15 }16}
1<?php23namespace App\Models;45use Illuminate\Database\Eloquent\Model;67class Post extends Model8{9 /**10 * Get all of the tags for the post.11 */12 public function tags()13 {14 return $this->morphToMany(Tag::class, 'taggable');15 }16}
定義反向的關聯
接著,在 Tag
Model 中,我們可以為 Tag 的各個可能的上層 Model 定義個別的方法。因此,在這個例子中,我們會定義一個 posts
方法與一個 videos
方法。這兩個方法都應回傳 morphedByMany
方法的結果。
morphedByMany
方法接受關聯 Model 的名稱,以及「關聯名稱」。根據我們設定給中介表的名稱以及其中包含的索引鍵,我們可以將關聯推導為「taggable」:
1<?php23namespace App\Models;45use Illuminate\Database\Eloquent\Model;67class Tag extends Model8{9 /**10 * Get all of the posts that are assigned this tag.11 */12 public function posts()13 {14 return $this->morphedByMany(Post::class, 'taggable');15 }1617 /**18 * Get all of the videos that are assigned this tag.19 */20 public function videos()21 {22 return $this->morphedByMany(Video::class, 'taggable');23 }24}
1<?php23namespace App\Models;45use Illuminate\Database\Eloquent\Model;67class Tag extends Model8{9 /**10 * Get all of the posts that are assigned this tag.11 */12 public function posts()13 {14 return $this->morphedByMany(Post::class, 'taggable');15 }1617 /**18 * Get all of the videos that are assigned this tag.19 */20 public function videos()21 {22 return $this->morphedByMany(Video::class, 'taggable');23 }24}
取得關聯
定義好資料庫資料表與 Model 後,就可以通過這些 Model 來存取關聯。舉例來說,若要取得一則貼文的 Tag,我們可以使用 tags
動態關聯屬性:
1use App\Models\Post;23$post = Post::find(1);45foreach ($post->tags as $tag) {6 //7}
1use App\Models\Post;23$post = Post::find(1);45foreach ($post->tags as $tag) {6 //7}
可以在多型子 Model 中通過存取呼叫 morphedByMany
的方法名稱來存取多型關聯的上層 Model。在這個例子中,就是 Tag
Model 上的 posts
與 videos
方法:
1use App\Models\Tag;23$tag = Tag::find(1);45foreach ($tag->posts as $post) {6 //7}89foreach ($tag->videos as $video) {10 //11}
1use App\Models\Tag;23$tag = Tag::find(1);45foreach ($tag->posts as $post) {6 //7}89foreach ($tag->videos as $video) {10 //11}
自訂多型型別
預設情況下,Laravel 會使用類別的完整格式名稱 (Fully Qualified Class Name) 來儲存關聯 Model 的「類型 (Type)」。具體而言,在上述的 One-to-Many 例子中,Comment
Model 可以隸屬於 Post
Model、也可以隸屬於 Video
Model,因此預設的 commentable_type
就分別會是 App\Models\Post
或 App\Models\Video
。不過,開發人員可能會想將這些值從專案的內部結構中解耦 (Decouple) 出來。
舉例來說,我們可以使用像 post
或 video
等簡單的字串作為「型別」,而不是使用 Model 名稱。這樣一來,即使我們修改了 Model 的名稱,資料庫中的多型「type」欄位值也會繼續有效:
1use Illuminate\Database\Eloquent\Relations\Relation;23Relation::enforceMorphMap([4 'post' => 'App\Models\Post',5 'video' => 'App\Models\Video',6]);
1use Illuminate\Database\Eloquent\Relations\Relation;23Relation::enforceMorphMap([4 'post' => 'App\Models\Post',5 'video' => 'App\Models\Video',6]);
可以在 App\Providers\AppServiceProvider
類別或依照需求自行的 Service Provider 中之 boot
方法內呼叫 enforceMorphMap
方法:
我們可以使用 Model 的 getMorphClass
方法來在執行階段判斷給定 Model 的 Morph 別名。相反的,我們可以使用 Relation::getMorphedModel
方法來取得 Morph 別名的完整格式類別名稱:
1use Illuminate\Database\Eloquent\Relations\Relation;23$alias = $post->getMorphClass();45$class = Relation::getMorphedModel($alias);
1use Illuminate\Database\Eloquent\Relations\Relation;23$alias = $post->getMorphClass();45$class = Relation::getMorphedModel($alias);
在專案中使用「Morph Map」時,所有的 morphable *_type
欄位值還是會保持原本的完整各式類別名稱,需要再更改為其「映射 (Map)」的名稱。
動態關聯
可以使用 resolveRelationUsing
方法來在執行階段定義 Eloquent Model 間的關聯。雖然對於一般的專案開發並不建議這麼做,但在開發 Laravel 套件的時候偶爾會很實用。
resolveRelationUsing
方法接受自訂的關聯名稱作為其第一個引述。第二個傳入該方法的引數應為閉包,該閉包應接受一個 Model 實體並回傳一個有效的 Eloquent 關聯定義。一般來說,應在某個 Service Provider 內的 boot 方法中定義動態關聯。
1use App\Models\Order;2use App\Models\Customer;34Order::resolveRelationUsing('customer', function ($orderModel) {5 return $orderModel->belongsTo(Customer::class, 'customer_id');6});
1use App\Models\Order;2use App\Models\Customer;34Order::resolveRelationUsing('customer', function ($orderModel) {5 return $orderModel->belongsTo(Customer::class, 'customer_id');6});
在定義動態關聯時,請總是提供顯式的索引鍵名稱給 Eloquent 關聯方法。
查詢關聯
由於所有的 Eloquent 關聯都是以方法來定義的,所以我們可以呼叫這些方法來取得關聯的實體,而不需執行查詢來載入關聯的 Model。此外,每種 Eloquent 關聯都可作為 Query Builder 使用,因此我們也能在最終向資料庫執行 SQL 查詢前往關聯查詢串上一些查詢條件。
舉例來說,假設我們有一個部落格網站,其中 User
Model 可以關聯到 Post
Model:
1<?php23namespace App\Models;45use Illuminate\Database\Eloquent\Model;67class User extends Model8{9 /**10 * Get all of the posts for the user.11 */12 public function posts()13 {14 return $this->hasMany(Post::class);15 }16}
1<?php23namespace App\Models;45use Illuminate\Database\Eloquent\Model;67class User extends Model8{9 /**10 * Get all of the posts for the user.11 */12 public function posts()13 {14 return $this->hasMany(Post::class);15 }16}
我們可以查詢 posts
關聯,並在關聯上像這樣加上額外的條件:
1use App\Models\User;23$user = User::find(1);45$user->posts()->where('active', 1)->get();
1use App\Models\User;23$user = User::find(1);45$user->posts()->where('active', 1)->get();
在關聯上我們可以使用任何的 Laravel Query Builder 方法,因此請確保有先閱讀過 Query Builder 的說明文件以瞭解有哪些方法可以使用。
在關聯後方串上 orWhere
子句
像上面的範例中一樣,在進行查詢的時候我們可以自由地往關聯新增查詢。不過,在將 orWhere
自居串上關聯時要注意,因為 orWhere
自居可能會被邏輯性地分組在與關聯條件相同的層級上:
1$user->posts()2 ->where('active', 1)3 ->orWhere('votes', '>=', 100)4 ->get();
1$user->posts()2 ->where('active', 1)3 ->orWhere('votes', '>=', 100)4 ->get();
上述的例子會產生下列的 SQL。如你所見,or
子句會讓查詢回傳 所有 大於 100 得票數的使用者。這個查詢不會被限制在任何特定使用者上:
1select *2from posts3where user_id = ? and active = 1 or votes >= 100
1select *2from posts3where user_id = ? and active = 1 or votes >= 100
在大多數的情況下,應該使用邏輯群組以將條件檢查放在括號中進行分組:
1use Illuminate\Database\Eloquent\Builder;23$user->posts()4 ->where(function (Builder $query) {5 return $query->where('active', 1)6 ->orWhere('votes', '>=', 100);7 })8 ->get();
1use Illuminate\Database\Eloquent\Builder;23$user->posts()4 ->where(function (Builder $query) {5 return $query->where('active', 1)6 ->orWhere('votes', '>=', 100);7 })8 ->get();
上述的例子會產生下列 SQL。可以注意到,查詢條件已正確地進行邏輯分組,且查詢有保持限制在特定使用者上:
1select *2from posts3where user_id = ? and (active = 1 or votes >= 100)
1select *2from posts3where user_id = ? and (active = 1 or votes >= 100)
關聯方法 V.s. 動態屬性
若不想在 Eloquent 關聯查詢上新增任何額外的查詢條件,則可以直接將關聯作為屬性一樣存取。舉例來說,接續使用我們的 User
與 Post
範例 Model,我們可以像這樣存取 User 的所有 Post:
1use App\Models\User;23$user = User::find(1);45foreach ($user->posts as $post) {6 //7}
1use App\Models\User;23$user = User::find(1);45foreach ($user->posts as $post) {6 //7}
動態屬性會被「延遲載入 (Lazy Loading)」,這表示,這些關聯資料只有在實際存取的時候才會被載入。也因此,開發人員常常會使用積極式載入來預先載入稍後會被存取的關聯。使用預先載入,就可以顯著地降低許多在載入 Model 關聯時會被執行的 SQL 查詢。
查詢存在的關聯
在取得 Model 紀錄時,我們可能會想依據關聯是否存在來限制查詢結果。舉例來說,假設我們想取得所有至少有一篇留言的部落格貼文。為此,我們可以將關聯的名稱傳入 has
或 orHas
方法中:
1use App\Models\Post;23// 取得所有至少有一篇留言的貼文...4$posts = Post::has('comments')->get();
1use App\Models\Post;23// 取得所有至少有一篇留言的貼文...4$posts = Post::has('comments')->get();
我們也可以指定一個運算子與總數來進一步自訂查詢:
1// 取得所有至少有 3 篇留言的貼文...2$posts = Post::has('comments', '>=', 3)->get();
1// 取得所有至少有 3 篇留言的貼文...2$posts = Post::has('comments', '>=', 3)->get();
可以使用「點 (.)」標記法來撰寫巢狀的 has
陳述式。舉例來說,我們可以取得所有至少有一篇含有圖片的留言的部落格貼文:
1// 取得有留言且這些留言至少有一張圖片的貼文...2$posts = Post::has('comments.images')->get();
1// 取得有留言且這些留言至少有一張圖片的貼文...2$posts = Post::has('comments.images')->get();
若需要更多功能,可以使用 whereHas
或 orWhereHas
方法來在 has
查詢上定義額外的查詢條件,如檢查留言的內容等:
1use Illuminate\Database\Eloquent\Builder;23// 取得至少有一篇留言內容包含文字 code% 的貼文...4$posts = Post::whereHas('comments', function (Builder $query) {5 $query->where('content', 'like', 'code%');6})->get();78// 取得至少有 10 篇留言內容包含文字 code% 的貼文...9$posts = Post::whereHas('comments', function (Builder $query) {10 $query->where('content', 'like', 'code%');11}, '>=', 10)->get();
1use Illuminate\Database\Eloquent\Builder;23// 取得至少有一篇留言內容包含文字 code% 的貼文...4$posts = Post::whereHas('comments', function (Builder $query) {5 $query->where('content', 'like', 'code%');6})->get();78// 取得至少有 10 篇留言內容包含文字 code% 的貼文...9$posts = Post::whereHas('comments', function (Builder $query) {10 $query->where('content', 'like', 'code%');11}, '>=', 10)->get();
由於 Eloquent 目前並不支援在多個資料庫間查詢關聯的存否,因此要查詢的關聯必須在同一個資料庫中。
內嵌的存在關聯查詢
若想要使用附加在關聯查詢上的簡單且單一的 Where 條件來查詢關聯的存否,那麼用 whereRelation
與 whereMorphRelation
方法應該會很方便。舉例來說,我們可以查詢所有有未審核 (Unapproved) 留言的貼文:
1use App\Models\Post;23$posts = Post::whereRelation('comments', 'is_approved', false)->get();
1use App\Models\Post;23$posts = Post::whereRelation('comments', 'is_approved', false)->get();
當然,就像呼叫 Query Builder 的 where
方法一樣,我們也可以指定運算子:
1$posts = Post::whereRelation(2 'comments', 'created_at', '>=', now()->subHour()3)->get();
1$posts = Post::whereRelation(2 'comments', 'created_at', '>=', now()->subHour()3)->get();
查詢不存在的關聯
在取得 Model 紀錄時,我們可能會想依據關聯的是否不存在來限制查詢結果。舉例來說,假設我們想取得所有 沒有 留言的部落格貼文。為此,我們可以將關聯的名稱傳入 doesntHave
或 orDoesntHave
方法中:
1use App\Models\Post;23$posts = Post::doesntHave('comments')->get();
1use App\Models\Post;23$posts = Post::doesntHave('comments')->get();
若需要更多功能,可以使用 whereDoesntHave
或 orWhereDoesntHave
方法來在 doesntHave
查詢上加上額外的查詢條件,如檢查留言的內容等:
1use Illuminate\Database\Eloquent\Builder;23$posts = Post::whereDoesntHave('comments', function (Builder $query) {4 $query->where('content', 'like', 'code%');5})->get();
1use Illuminate\Database\Eloquent\Builder;23$posts = Post::whereDoesntHave('comments', function (Builder $query) {4 $query->where('content', 'like', 'code%');5})->get();
我們也可以使用「點 (.)」標記法來對巢狀關聯進行查詢。舉例來說,下列查詢會取得所有沒有留言的貼文。不過,具有未禁言作者發表留言的文章也會被包含在結果裡面:
1use Illuminate\Database\Eloquent\Builder;23$posts = Post::whereDoesntHave('comments.author', function (Builder $query) {4 $query->where('banned', 0);5})->get();
1use Illuminate\Database\Eloquent\Builder;23$posts = Post::whereDoesntHave('comments.author', function (Builder $query) {4 $query->where('banned', 0);5})->get();
查詢 Morph To 關聯
若要查詢「Morph To」關聯是否存在,可以使用 whereHasMorph
或 whereDoesntHaveMorph
方法。這些方法都接受關聯名稱作為其第一個引數。接著,這個方法還接受要被包含在查詢裡的關聯 Model 名稱。最後,我們還可以提供用來自訂關聯查詢的閉包:
1use App\Models\Comment;2use App\Models\Post;3use App\Models\Video;4use Illuminate\Database\Eloquent\Builder;56// 取得至少有一篇留言的貼文或影片,且貼文或影片的標題包含文字 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();1415// 取得至少有一篇留言的貼文或影片,且貼文或影片的標題不包含文字 code%...16$comments = Comment::whereDoesntHaveMorph(17 'commentable',18 Post::class,19 function (Builder $query) {20 $query->where('title', 'like', 'code%');21 }22)->get();
1use App\Models\Comment;2use App\Models\Post;3use App\Models\Video;4use Illuminate\Database\Eloquent\Builder;56// 取得至少有一篇留言的貼文或影片,且貼文或影片的標題包含文字 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();1415// 取得至少有一篇留言的貼文或影片,且貼文或影片的標題不包含文字 code%...16$comments = Comment::whereDoesntHaveMorph(17 'commentable',18 Post::class,19 function (Builder $query) {20 $query->where('title', 'like', 'code%');21 }22)->get();
有時候,我們可能會想依據多型關聯 Model 的「類型」來新增查詢條件。傳給 whereHasMorph
方法的閉包可接受一個 $type
值作為其第二個引述。使用 $type
引述,就可以檢查正在建立的查詢是什麼「類型」:
1use Illuminate\Database\Eloquent\Builder;23$comments = Comment::whereHasMorph(4 'commentable',5 [Post::class, Video::class],6 function (Builder $query, $type) {7 $column = $type === Post::class ? 'content' : 'title';89 $query->where($column, 'like', 'code%');10 }11)->get();
1use Illuminate\Database\Eloquent\Builder;23$comments = Comment::whereHasMorph(4 'commentable',5 [Post::class, Video::class],6 function (Builder $query, $type) {7 $column = $type === Post::class ? 'content' : 'title';89 $query->where($column, 'like', 'code%');10 }11)->get();
查詢所有關聯的 Model
我們可以提供 *
作為萬用字元,而不需以陣列列出所有可能的多型 Model。這樣以來 Laravel 就會從資料庫中取得所有可能的多型類型。Laravel 會執行一個額外的查詢來進行此行動:
1use Illuminate\Database\Eloquent\Builder;23$comments = Comment::whereHasMorph('commentable', '*', function (Builder $query) {4 $query->where('title', 'like', 'foo%');5})->get();
1use Illuminate\Database\Eloquent\Builder;23$comments = Comment::whereHasMorph('commentable', '*', function (Builder $query) {4 $query->where('title', 'like', 'foo%');5})->get();
彙總關聯的 Model
計數關聯的 Model
有時候我們可能會想知道給定關聯中關聯 Model 的數量,但又不想真正載入這些 Model。為此,我們可以使用 withCount
方法。withCount
方法會在查詢結果的 Model 中加上一個 {關聯}_count
屬性:
1use App\Models\Post;23$posts = Post::withCount('comments')->get();45foreach ($posts as $post) {6 echo $post->comments_count;7}
1use App\Models\Post;23$posts = Post::withCount('comments')->get();45foreach ($posts as $post) {6 echo $post->comments_count;7}
只要將陣列傳入 withCount
方法,就可以為多個關聯「計數」,或是在查詢上加上額外的查詢條件:
1use Illuminate\Database\Eloquent\Builder;23$posts = Post::withCount(['votes', 'comments' => function (Builder $query) {4 $query->where('content', 'like', 'code%');5}])->get();67echo $posts[0]->votes_count;8echo $posts[0]->comments_count;
1use Illuminate\Database\Eloquent\Builder;23$posts = Post::withCount(['votes', 'comments' => function (Builder $query) {4 $query->where('content', 'like', 'code%');5}])->get();67echo $posts[0]->votes_count;8echo $posts[0]->comments_count;
也可以為關聯總數結果加上別名,這樣就能對單一關聯計算多次數量:
1use Illuminate\Database\Eloquent\Builder;23$posts = Post::withCount([4 'comments',5 'comments as pending_comments_count' => function (Builder $query) {6 $query->where('approved', false);7 },8])->get();910echo $posts[0]->comments_count;11echo $posts[0]->pending_comments_count;
1use Illuminate\Database\Eloquent\Builder;23$posts = Post::withCount([4 'comments',5 'comments as pending_comments_count' => function (Builder $query) {6 $query->where('approved', false);7 },8])->get();910echo $posts[0]->comments_count;11echo $posts[0]->pending_comments_count;
延後 (Deferred) 數量計算的載入
使用 loadCount
方法,就可以在上層 Model 已經載入後再接著載入關聯的計數:
1$book = Book::first();23$book->loadCount('genres');
1$book = Book::first();23$book->loadCount('genres');
若想在計數查詢上設定額外的查詢條件,可以傳入一組陣列,其索引鍵應為要計數的關聯。陣列的值則為一個閉包,用來接收 Query Builder 實體:
1$book->loadCount(['reviews' => function ($query) {2 $query->where('rating', 5);3}])
1$book->loadCount(['reviews' => function ($query) {2 $query->where('rating', 5);3}])
關聯計數與自訂 Select 陳述式
若想組合使用 withCount
與 select
陳述式,請在 select
方法後再呼叫 withCount
:
1$posts = Post::select(['title', 'body'])2 ->withCount('comments')3 ->get();
1$posts = Post::select(['title', 'body'])2 ->withCount('comments')3 ->get();
其他彙總函式
除了 withCount
方法外,Eloquent 也提供了 withMin
, withMax
, withAvg
, withSum
, 與 withExists
等方法。這些方法會在查詢結果的 Model 上加上一個 {關聯}_{函式}_{欄位}
屬性:
1use App\Models\Post;23$posts = Post::withSum('comments', 'votes')->get();45foreach ($posts as $post) {6 echo $post->comments_sum_votes;7}
1use App\Models\Post;23$posts = Post::withSum('comments', 'votes')->get();45foreach ($posts as $post) {6 echo $post->comments_sum_votes;7}
若想使用另一個名稱來存取彙總函式的結果,可自行指定別名:
1$posts = Post::withSum('comments as total_comments', 'votes')->get();23foreach ($posts as $post) {4 echo $post->total_comments;5}
1$posts = Post::withSum('comments as total_comments', 'votes')->get();23foreach ($posts as $post) {4 echo $post->total_comments;5}
與 loadCount
方法類似,Eloquent 中也有這些方法的延遲 (Deferred) 版本。可以在已經取得的 Eloquent Model 上進行這些額外的彙總運算:
1$post = Post::first();23$post->loadSum('comments', 'votes');
1$post = Post::first();23$post->loadSum('comments', 'votes');
若想組合使用這些彙總與 select
陳述式,請在 select
方法後再呼叫這些彙總函式:
1$posts = Post::select(['title', 'body'])2 ->withExists('comments')3 ->get();
1$posts = Post::select(['title', 'body'])2 ->withExists('comments')3 ->get();
在 Morph To 關聯上計算關聯 Model 的數量
若想積極式載入「Morph to」關聯、或是關聯 Model 計數等由關聯回傳的功能,可以使用 morphTo
關聯的 morphWithCount
方法,並搭配 with
方法使用。
在這個例子中,我們假設 Photo
與 Post
Model 會建立 ActivityFeed
Model。假設 ActivityFeed
Model 定義一個名為 parentable
的「Morph to」關聯,可讓使用者在某一 ActivityFeed
實體上取得上層的 Photo
或 Post
Model。此外,我們也假設 Photo
Model「Have Many (有多個)」 Tag
Model,而 Post
Model「Have Many」Comment
Model。
接著,來假設我們現在要去的 ActivityFeed
實體,並為取得的每個 ActivityFeed
實體積極式載入 parentable
上層 Model。此外,我們也想知道上層的每張圖片各有多少個 Tag、還有上層的每篇貼文各有多少則留言:
1use Illuminate\Database\Eloquent\Relations\MorphTo;23$activities = ActivityFeed::with([4 'parentable' => function (MorphTo $morphTo) {5 $morphTo->morphWithCount([6 Photo::class => ['tags'],7 Post::class => ['comments'],8 ]);9 }])->get();
1use Illuminate\Database\Eloquent\Relations\MorphTo;23$activities = ActivityFeed::with([4 'parentable' => function (MorphTo $morphTo) {5 $morphTo->morphWithCount([6 Photo::class => ['tags'],7 Post::class => ['comments'],8 ]);9 }])->get();
延後 (Deferred) 數量計算的載入
假設我們已經取得 ActivityFeed
Model (活動摘要),接著,我們想要載入與活動摘要關聯的各種 parentable
Model 的巢狀關聯數量。我們可以使用 loadMorphCount
方法來完成:
1$activities = ActivityFeed::with('parentable')->get();23$activities->loadMorphCount('parentable', [4 Photo::class => ['tags'],5 Post::class => ['comments'],6]);
1$activities = ActivityFeed::with('parentable')->get();23$activities->loadMorphCount('parentable', [4 Photo::class => ['tags'],5 Post::class => ['comments'],6]);
積極式載入
以屬性方式存取 Eloquent 關聯時,關聯的 Model 會被「消極式載入 (Lazy Load)」。這表示,直到首次存取該屬性前,關聯資料都不會被載入。不過,Eloquent 也可以在查詢上層 Model 時就「積極式載入 (Eager Load)」關聯。積極式載入可以減少「N + 1」問題。為了示範什麼是 N + 1 問題,我們先假設有個「隸屬於 (Belongs to)」Author
Model 的 Book
Model:
1<?php23namespace App\Models;45use Illuminate\Database\Eloquent\Model;67class Book extends Model8{9 /**10 * Get the author that wrote the book.11 */12 public function author()13 {14 return $this->belongsTo(Author::class);15 }16}
1<?php23namespace App\Models;45use Illuminate\Database\Eloquent\Model;67class Book extends Model8{9 /**10 * Get the author that wrote the book.11 */12 public function author()13 {14 return $this->belongsTo(Author::class);15 }16}
現在,我們來取得所有書籍與其作者:
1use App\Models\Book;23$books = Book::all();45foreach ($books as $book) {6 echo $book->author->name;7}
1use App\Models\Book;23$books = Book::all();45foreach ($books as $book) {6 echo $book->author->name;7}
這個迴圈會執行一個查詢來取得資料表中所有的書籍,然後每本書都會再執行一個查詢來取得書籍的作者。因此,若我們有 25 本書,上述程式碼就會執行 26 筆資料庫查詢:1 個查詢來取得書籍,另外 25 個額外的查詢來取得每本書的作者。
幸好,我們可以使用積極式載入來把這一連串行動降低為只需要 2 個查詢。在建立查詢時,可以使用 with
方法來指定哪個關聯要被積極式載入:
1$books = Book::with('author')->get();23foreach ($books as $book) {4 echo $book->author->name;5}
1$books = Book::with('author')->get();23foreach ($books as $book) {4 echo $book->author->name;5}
這樣一來,就只會執行 2 個查詢 —— 一個查詢去的所有的書籍,另一個查詢則取得所有書籍的作者。
1select * from books23select * from authors where id in (1, 2, 3, 4, 5, ...)
1select * from books23select * from authors where id in (1, 2, 3, 4, 5, ...)
積極式載入多個關聯
有時候,我們可能需要積極式載入多個不同的關聯。要載入多個不同的關聯,只需要傳入一組包含關聯的陣列給 with
方法即可:
1$books = Book::with(['author', 'publisher'])->get();
1$books = Book::with(['author', 'publisher'])->get();
巢狀積極式載入
若要積極載入關聯的關聯,可以使用「點 (.)」標記法。舉例來說,讓我們來積極載入所有書籍的作者,以及所有作者的聯絡方式 (Contact):
1$books = Book::with('author.contacts')->get();
1$books = Book::with('author.contacts')->get();
積極載入巢狀的 morphTo
關聯
若想積極載入 morphTo
關聯、或是巢狀的關聯等由 morphTo 關聯回傳的功能,可以使用 morphTo
關聯的 morphWith
方法,並搭配 with
方法使用。為了讓我們更瞭解這個功能,我們先來看看下列 Model:
1<?php23use Illuminate\Database\Eloquent\Model;45class ActivityFeed extends Model6{7 /**8 * Get the parent of the activity feed record.9 */10 public function parentable()11 {12 return $this->morphTo();13 }14}
1<?php23use Illuminate\Database\Eloquent\Model;45class ActivityFeed extends Model6{7 /**8 * Get the parent of the activity feed record.9 */10 public function parentable()11 {12 return $this->morphTo();13 }14}
在這個例子中,先假設 Event
, Photo
, 與 Post
會建立 ActivityFeed
Model。另外,也來假設 Event
Model 隸屬於 Calendar
Model,而 Photo
Model 則與 Tag
Model 相關聯,然後 Post
Model 隸屬於 Author
Model。
有了這些 Model 定義與關聯,我們就可以取得 ActivityFeed
Model 實體,然後積極載入所有 parentable
Model 與這些 parentable
Model 的巢狀關聯:
1use Illuminate\Database\Eloquent\Relations\MorphTo;23$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();
1use Illuminate\Database\Eloquent\Relations\MorphTo;23$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();
1$books = Book::with('author:id,name,book_id')->get();
使用這個功能時,請務必在欄位列表中包含 id
欄位以及其他相關的外部索引鍵欄位。
預設積極載入
對於某些 Model,我們可能會希望這個 Model 總是能載入一些關聯。為此,我們可以在這種 Model 上定義一個 $with
屬性:
1<?php23namespace App\Models;45use Illuminate\Database\Eloquent\Model;67class Book extends Model8{9 /**10 * The relationships that should always be loaded.11 *12 * @var array13 */14 protected $with = ['author'];1516 /**17 * Get the author that wrote the book.18 */19 public function author()20 {21 return $this->belongsTo(Author::class);22 }2324 /**25 * Get the genre of the book.26 */27 public function genre()28 {29 return $this->belongsTo(Genre::class);30 }31}
1<?php23namespace App\Models;45use Illuminate\Database\Eloquent\Model;67class Book extends Model8{9 /**10 * The relationships that should always be loaded.11 *12 * @var array13 */14 protected $with = ['author'];1516 /**17 * Get the author that wrote the book.18 */19 public function author()20 {21 return $this->belongsTo(Author::class);22 }2324 /**25 * Get the genre of the book.26 */27 public function genre()28 {29 return $this->belongsTo(Genre::class);30 }31}
若想為單一查詢移除 $with
屬性中的某個項目,可以使用 without
方法:
1$books = Book::without('author')->get();
1$books = Book::without('author')->get();
若想為單一查詢複寫 $with
屬性中的所有項目,可以使用 withOnly
方法:
1$books = Book::withOnly('genre')->get();
1$books = Book::withOnly('genre')->get();
包含查詢條件的積極載入
在積極載入關聯時,我們有時候可能會希望能給積極載入查詢指定額外的查詢條件。可以通過傳入一組包含關聯的陣列給 with
方法來達成。這個陣列的索引鍵應為關聯的名稱,而陣列值則為要給積極載入查詢加上額外查詢條件的閉包:
1use App\Models\User;23$users = User::with(['posts' => function ($query) {4 $query->where('title', 'like', '%code%');5}])->get();
1use App\Models\User;23$users = User::with(['posts' => function ($query) {4 $query->where('title', 'like', '%code%');5}])->get();
在這個例子中,Eloquent 只會積極載入 title
欄位含有關鍵字 code
的文章。你還可以呼叫其他的 Query Builder 方法來進一步自訂積極式載入:
1$users = User::with(['posts' => function ($query) {2 $query->orderBy('created_at', 'desc');3}])->get();
1$users = User::with(['posts' => function ($query) {2 $query->orderBy('created_at', 'desc');3}])->get();
積極式載入不能使用 limit
與 take
Query Builder 方法來作條件限制。
包含查詢條件的 morphTo
關聯積極載入
在積極載入 morphTo
關聯時,Eloquent 會為關聯 Model 的每個類型都執行多筆查詢。我們可以使用 MorphTo
關聯的 constrain
方法來對這些查詢分別加上額外的查詢條件:
1use Illuminate\Database\Eloquent\Builder;2use Illuminate\Database\Eloquent\Relations\MorphTo;34$comments = Comment::with(['commentable' => function (MorphTo $morphTo) {5 $morphTo->constrain([6 Post::class => function (Builder $query) {7 $query->whereNull('hidden_at');8 },9 Video::class => function (Builder $query) {10 $query->where('type', 'educational');11 },12 ]);13}])->get();
1use Illuminate\Database\Eloquent\Builder;2use Illuminate\Database\Eloquent\Relations\MorphTo;34$comments = Comment::with(['commentable' => function (MorphTo $morphTo) {5 $morphTo->constrain([6 Post::class => function (Builder $query) {7 $query->whereNull('hidden_at');8 },9 Video::class => function (Builder $query) {10 $query->where('type', 'educational');11 },12 ]);13}])->get();
在這個範例中,Eloquent 只會積極載入非隱藏的貼文,以及 type
值不是「educational」的影片。
消極的積極式載入
有時候,我們可能需要在已取得上層 Model 後才積極載入某個關聯。舉例來說,當想動態決定是否要載入關聯 Model 時,這種功能特別適合:
1use App\Models\Book;23$books = Book::all();45if ($someCondition) {6 $books->load('author', 'publisher');7}
1use App\Models\Book;23$books = Book::all();45if ($someCondition) {6 $books->load('author', 'publisher');7}
若想在積極載入查詢上設定額外的查詢條件,可以傳入一組陣列,其索引鍵應為要載入的關聯。陣列的值則為一個閉包,用來接收 Query Builder 實體:
1$author->load(['books' => function ($query) {2 $query->orderBy('published_date', 'asc');3}]);
1$author->load(['books' => function ($query) {2 $query->orderBy('published_date', 'asc');3}]);
若想只在某個關聯未被載入時才載入該關聯,可使用 loadMissing
方法:
1$book->loadMissing('author');
1$book->loadMissing('author');
巢狀之消極的積極載入與 morphTo
若想積極式載入 morphTo
關聯、或是關聯 Model 的巢狀關聯等由 morphTo 關聯所回傳的功能,可以使用 loadMorph
方法:
這個方法的第一個引數是 morphTo
關聯的名稱,第二個引數則是一組包含 Model / 關聯配對的陣列。為了說明這個功能,先來看看下列 Model:
1<?php23use Illuminate\Database\Eloquent\Model;45class ActivityFeed extends Model6{7 /**8 * Get the parent of the activity feed record.9 */10 public function parentable()11 {12 return $this->morphTo();13 }14}
1<?php23use Illuminate\Database\Eloquent\Model;45class ActivityFeed extends Model6{7 /**8 * Get the parent of the activity feed record.9 */10 public function parentable()11 {12 return $this->morphTo();13 }14}
在這個例子中,先假設 Event
, Photo
, 與 Post
會建立 ActivityFeed
Model。另外,也來假設 Event
Model 隸屬於 Calendar
Model,而 Photo
Model 則與 Tag
Model 相關聯,然後 Post
Model 隸屬於 Author
Model。
有了這些 Model 定義與關聯,我們就可以取得 ActivityFeed
Model 實體,然後積極載入所有 parentable
Model 與這些 parentable
Model 的巢狀關聯:
1$activities = ActivityFeed::with('parentable')2 ->get()3 ->loadMorph('parentable', [4 Event::class => ['calendar'],5 Photo::class => ['tags'],6 Post::class => ['author'],7 ]);
1$activities = ActivityFeed::with('parentable')2 ->get()3 ->loadMorph('parentable', [4 Event::class => ['calendar'],5 Photo::class => ['tags'],6 Post::class => ['author'],7 ]);
預防消極載入
前面也說明過,對你的專案來說,積極載入關聯通常可以顯著提升效能。因此,我們可能會希望讓 Laravel 總是避免消極式載入關聯。為此,我們可以呼叫基礎 Eloquent Model 上的 preventLazyLoading
方法。一般來說,應該在你的專案中 AppServiceProvider
類別的 boot
方法內呼叫這個方法。
preventLazyLoading
方法接受一個可選的布林引數,用來判斷是否應防止消極式載入。舉例來說,我們肯跟會希望只在非正式環境下才進用消極式載入,這樣一來,就算正式環境上的程式碼內不小心有個消極式載入的關聯,正式環境也可以正常運作:
1use Illuminate\Database\Eloquent\Model;23/**4 * Bootstrap any application services.5 *6 * @return void7 */8public function boot()9{10 Model::preventLazyLoading(! $this->app->isProduction());11}
1use Illuminate\Database\Eloquent\Model;23/**4 * Bootstrap any application services.5 *6 * @return void7 */8public function boot()9{10 Model::preventLazyLoading(! $this->app->isProduction());11}
阻止消極式載入後,當程式嘗試要消極載入任何 Eloquent 關聯時,Eloquent 會擲回一個 Illuminate\Database\LazyLoadingViolationException
例外。
可以使用 handleLazyLoadingViolationsUsing
方法來自訂當發生消極載入時要如何處置。舉例來說,我們可以使用這個方法來讓 Laravel 在遇到消極載入的時候紀錄到日誌,而不是使用例外在終止程式的執行:
1Model::handleLazyLoadingViolationUsing(function ($model, $relation) {2 $class = get_class($model);34 info("Attempted to lazy load [{$relation}] on model [{$class}].");5});
1Model::handleLazyLoadingViolationUsing(function ($model, $relation) {2 $class = get_class($model);34 info("Attempted to lazy load [{$relation}] on model [{$class}].");5});
插入與更新關聯的 Model
save
方法
Eloquent 提供了一些方便的方法來給關聯新增新 Model。舉例來說,我們可能會需要給貼文新增新留言。比起手動在 Comment
Model 上設定 post_id
,我們可以使用關聯的 save
Model 來插入留言:
1use App\Models\Comment;2use App\Models\Post;34$comment = new Comment(['message' => 'A new comment.']);56$post = Post::find(1);78$post->comments()->save($comment);
1use App\Models\Comment;2use App\Models\Post;34$comment = new Comment(['message' => 'A new comment.']);56$post = Post::find(1);78$post->comments()->save($comment);
請注意,我們不是以動態屬性的方式來存取 comment
關聯,而是呼叫 comments
方法來取得關聯的實體。save
方法會自動為新建立的 Comment
Model 加上適當的 post_id
值。
若有需要保存多個關聯 Model,可以使用 saveMany
方法:
1$post = Post::find(1);23$post->comments()->saveMany([4 new Comment(['message' => 'A new comment.']),5 new Comment(['message' => 'Another new comment.']),6]);
1$post = Post::find(1);23$post->comments()->saveMany([4 new Comment(['message' => 'A new comment.']),5 new Comment(['message' => 'Another new comment.']),6]);
save
與 saveMany
會將 Model 實體保存起來。不過,保存好的 Model 並不會被加到上層 Model 中已經載入到記憶體的關聯。在使用 save
或 saveMany
方法後,若有打算要存取這些關聯,可使用 refresh
方法來重新載入 Model 與其關聯:
1$post->comments()->save($comment);23$post->refresh();45// 所有留言,包含新建立的留言...6$post->comments;
1$post->comments()->save($comment);23$post->refresh();45// 所有留言,包含新建立的留言...6$post->comments;
遞歸保存 Model 與關聯
若想讓 save
方法保存 Model 與其所有相關的關聯 Model,可以使用 push
方法。在這個例子中,Post
Model、Post
Model 的留言、留言的作者等都會一起被保存:
1$post = Post::find(1);23$post->comments[0]->message = 'Message';4$post->comments[0]->author->name = 'Author Name';56$post->push();
1$post = Post::find(1);23$post->comments[0]->message = 'Message';4$post->comments[0]->author->name = 'Author Name';56$post->push();
create
方法
除了 save
跟 saveMany
方法外,也可以使用 create
方法來建立 Model 並插入資料庫。create
方法接受一組包含屬性的陣列。save
與 create
間不同的地方在於:save
接收完整的 Eloquent Model 實體,而 create
接收的是純 PHP 的 array
。create
方法會回傳新建立的 Model:
1use App\Models\Post;23$post = Post::find(1);45$comment = $post->comments()->create([6 'message' => 'A new comment.',7]);
1use App\Models\Post;23$post = Post::find(1);45$comment = $post->comments()->create([6 'message' => 'A new comment.',7]);
可以使用 createMany
方法來建立多個關聯的 Model:
1$post = Post::find(1);23$post->comments()->createMany([4 ['message' => 'A new comment.'],5 ['message' => 'Another new comment.'],6]);
1$post = Post::find(1);23$post->comments()->createMany([4 ['message' => 'A new comment.'],5 ['message' => 'Another new comment.'],6]);
也可以使用 findOrNew
, firstOrNew
, firstOrCreate
, 與 updateOrCreate
等方法來在關聯上建立並更新 Model。
在使用 create
方法前,請先閱讀大量賦值的說明文件。
Belongs To 關聯
若想將子 Model 指派給新的上層 Model,可以使用 associate
方法。在這個例子中,User
Model 定義了一個連到 Account
Model 的 belongsTo
關聯。associate
方法會在子 Model 上設定外部索引鍵:
1use App\Models\Account;23$account = Account::find(10);45$user->account()->associate($account);67$user->save();
1use App\Models\Account;23$account = Account::find(10);45$user->account()->associate($account);67$user->save();
若要從子 Model 上移除上層 Model,可以使用 dissociate
方法。這個方法會將關聯的外部索引鍵設為 null
:
1$user->account()->dissociate();23$user->save();
1$user->account()->dissociate();23$user->save();
Many To Many 關聯
附加 / 解除附加
Eloquent 還提供一些能讓處理多對多關聯更方便的方法。舉例來說,先假設一個使用者 (User) 可以有多個職位 (Role),而一個職位可以有多個使用者。可以使用 attach
方法來將某個職位附加到使用者身上,attach
會在關聯的中介資料表上插入一筆紀錄來完成:
1use App\Models\User;23$user = User::find(1);45$user->roles()->attach($roleId);
1use App\Models\User;23$user = User::find(1);45$user->roles()->attach($roleId);
在把關聯附加到 Model 上時,可以傳入一組陣列,包含額外要被插入到中介資料表上的資料:
1$user->roles()->attach($roleId, ['expires' => $expires]);
1$user->roles()->attach($roleId, ['expires' => $expires]);
有時候,我們還會需要從使用者身上移除某個職位。若要移除 Many-to-Many 關聯的紀錄,請使用 detach
方法。detach
方法會從中介資料表上移除相應的紀錄。不過,使用者跟職位兩個 Model 都還會保留在資料庫中:
1// 在使用者上取消附加單一職位...2$user->roles()->detach($roleId);34// 在使用者上取消附加所有職位...5$user->roles()->detach();
1// 在使用者上取消附加單一職位...2$user->roles()->detach($roleId);34// 在使用者上取消附加所有職位...5$user->roles()->detach();
為了更方便使用,attach
與 detach
也能接受一組包含 ID 的陣列作為輸入:
1$user = User::find(1);23$user->roles()->detach([1, 2, 3]);45$user->roles()->attach([6 1 => ['expires' => $expires],7 2 => ['expires' => $expires],8]);
1$user = User::find(1);23$user->roles()->detach([1, 2, 3]);45$user->roles()->attach([6 1 => ['expires' => $expires],7 2 => ['expires' => $expires],8]);
同步關聯
可以使用 sync
方法來設定 Many-to-Many 關聯。sync
方法接受一組包含 ID 的陣列,用以插入中介資料表。中介資料表中若有不在此陣列中的 ID 則會被移除。因此,完成這個操作後,中介資料表中就只會有給定陣列中的 ID:
1$user->roles()->sync([1, 2, 3]);
1$user->roles()->sync([1, 2, 3]);
也可以使用 ID 來傳入額外的中介資料表值:
1$user->roles()->sync([1 => ['expires' => true], 2, 3]);
1$user->roles()->sync([1 => ['expires' => true], 2, 3]);
如喔想為每個同步的 Model ID 都插入相同的中介資料表值,則可以使用 syncWithPivotValue
方法:
1$user->roles()->syncWithPivotValues([1, 2, 3], ['active' => true]);
1$user->roles()->syncWithPivotValues([1, 2, 3], ['active' => true]);
若想從給定陣列中移除現有的 ID,則可以使用 syncWithoutDetaching
方法:
1$user->roles()->syncWithoutDetaching([1, 2, 3]);
1$user->roles()->syncWithoutDetaching([1, 2, 3]);
切換關聯
Many-to-Many 關聯還提供了一個 toggle
方法,可以用來「切換 (Toggle)」給定關聯 Model ID 的附加狀態。若給定的 ID 目前是已附加的狀態,則該 ID 會被解除附加。反之,若目前未附加,則會被附加上去:
1$user->roles()->toggle([1, 2, 3]);
1$user->roles()->toggle([1, 2, 3]);
更新中介資料表上的紀錄
若想更新關聯的中介資料表上現有的紀錄,可以使用 updateExistingPivot
方法。這個方法接受中介資料表的外部索引鍵以及一組包含要更新屬性的陣列:
1$user = User::find(1);23$user->roles()->updateExistingPivot($roleId, [4 'active' => false,5]);
1$user = User::find(1);23$user->roles()->updateExistingPivot($roleId, [4 'active' => false,5]);
更新上層的時戳
若某 Model 有定義對另一個 Model 的 belongsTo
或 belongsToMany
關聯 —— 如 Comment
Model 隸屬於 Post
Model 等 —— 有時候,若能在子 Model 更新時也一併更新上層 Model 的時戳會很實用。
舉例來說,當 Comment
Model 更新後,我們可能會想自動「更新 (Touch)」擁有該 Comment
的 Post
Model 上的 updated_at
時戳,將該時戳設為目前的日期與時間。為此,我們可以在子 Model 內新增一個 touches
屬性,其中包含關聯的名稱。當子 Model 更新後,這些關聯的 updated_at
時戳也會一起更新:
1<?php23namespace App\Models;45use Illuminate\Database\Eloquent\Model;67class Comment extends Model8{9 /**10 * All of the relationships to be touched.11 *12 * @var array13 */14 protected $touches = ['post'];1516 /**17 * Get the post that the comment belongs to.18 */19 public function post()20 {21 return $this->belongsTo(Post::class);22 }23}
1<?php23namespace App\Models;45use Illuminate\Database\Eloquent\Model;67class Comment extends Model8{9 /**10 * All of the relationships to be touched.11 *12 * @var array13 */14 protected $touches = ['post'];1516 /**17 * Get the post that the comment belongs to.18 */19 public function post()20 {21 return $this->belongsTo(Post::class);22 }23}
只有在使用 Eloquent 的 save
方法來更新子 Model 時,才會更新上傳 Model 的時戳。