[ Laravel 5.8 文件 ] Eloquent ORM —— 關聯關係
簡介
資料表經常要與其它表做關聯,比如一篇部落格文章可能有很多評論,或者一個訂單會被關聯到下單使用者,Eloquent 讓組織和處理這些關聯關係變得簡單,並且支援多種不同型別的關聯關係:
定義關聯關係
Eloquent 關聯關係以 Eloquent 模型類方法的方式定義。和 Eloquent 模型本身一樣,關聯關係也是強大的查詢構建器,定義關聯關係為方法可以提供功能強大的方法鏈和查詢能力。例如,我們可以新增更多約束條件到posts
關聯關係:
$user->posts()->where('active', 1)->get();
不過,在深入使用關聯關係之前,讓我們先學習如何定義每種關聯型別。
一對一
一對一關聯是一個非常簡單的關聯關係,例如,一個User
模型有一個與之關聯的Phone
模型。要定義這種關聯關係,我們需要將phone
方法置於User
模型中,phone
方法會呼叫Illuminate\Database\Eloquent\Concerns\HasRelationships
trait 中的hasOne
方法並返回其結果:
<?php namespace App; use Illuminate\Database\Eloquent\Model; class User extends Model{ /** * 獲取關聯到使用者的手機 */ public function phone() { return $this->hasOne('App\Phone'); } }
傳遞給hasOne
方法的第一個引數是關聯模型的名稱,關聯關係被定義後,我們可以使用 Eloquent 的動態屬性獲取關聯記錄。動態屬性允許我們訪問關聯方法,就像它們是定義在模型上的屬性一樣:
$phone = User::find(1)->phone;
Eloquent 預設關聯關係的外來鍵基於模型名稱,在本例中,Phone
模型預設有一個user_id
外來鍵,如果你希望覆蓋這種約定,可以傳遞第二個引數到hasOne
方法:
return $this->hasOne('App\Phone', 'foreign_key');
此外,Eloquent 假設外來鍵應該在父級上有一個與之匹配的id
(或者自定義$primaryKey
),換句話說,Eloquent 將會通過user
表的id
值去phone
表中查詢user_id
與之匹配的Phone
記錄。如果你想要關聯關係使用其他值而不是id
,可以傳遞第三個引數到hasOne
來指定自定義的主鍵:
return $this->hasOne('App\Phone', 'foreign_key', 'local_key');
我們通過傳遞完整引數改寫上述示例程式碼就是:
return $this->hasOne('App\Phone', 'user_id', 'id');
定義相對的關聯
我們可以從User
中訪問Phone
模型,相應地,也可以在Phone
模型中定義關聯關係從而讓我們可以擁有該手機的User
。我們可以使用belongsTo
方法定義與hasOne
關聯關係相對的關聯:
<?php namespace App; use Illuminate\Database\Eloquent\Model; class Phone extends Model{ /** * 獲取擁有該手機的使用者 */ public function user() { return $this->belongsTo('App\User'); } }
在上面的例子中,Eloquent 預設將會嘗試通過Phone
模型的user_id
去User
模型查詢與之匹配的記錄。Eloquent 通過在關聯關係方法名後加_id
字尾來生成預設的外來鍵名。不過,如果Phone
模型上的外來鍵不是user_id
,也可以將自定義的鍵名作為第二個引數傳遞到belongsTo
方法:
/** * 獲取手機對應的使用者 */ public function user(){ return $this->belongsTo('App\User', 'foreign_key'); }
如果父模型不使用id
作為主鍵,或者你希望使用別的資料列來連線子模型,可以將父表自定義鍵作為第三個引數傳遞給belongsTo
方法:
/** * 獲取手機對應的使用者 */ public function user(){ return $this->belongsTo('App\User', 'foreign_key', 'other_key'); }
同樣,我們通過傳遞完整的引數來改寫上述示例程式碼:
return $this->belongsTo('App\User', 'user_id', 'id');
一對多
“一對多”關聯是用於定義單個模型擁有多個其它模型的關聯關係。例如,一篇部落格文章擁有多條評論,和其他關聯關係一樣,一對多關聯通過在 Eloquent 模型中定義方法來定義:
<?php namespace App; use Illuminate\Database\Eloquent\Model; class Post extends Model{ /** * 獲取部落格文章的評論 */ public function comments() { return $this->hasMany('App\Comment'); } }
記住,Eloquent 會自動判斷Comment
模型的外來鍵,為方便起見,Eloquent 將擁有者模型名稱加上_id
字尾作為外來鍵。因此,在本例中,Eloquent 假設Comment
模型上的外來鍵是post_id
。
關聯關係被定義後,我們就可以通過訪問comments
屬性來訪問評論集合。由於 Eloquent 提供了“動態屬性”,我們可以像訪問模型的屬性一樣訪問關聯方法:
$comments = App\Post::find(1)->comments; foreach ($comments as $comment) { // }
當然,由於所有關聯同時也是查詢構建器,我們可以新增更多的條件約束到通過呼叫comments
方法獲取到的評論上:
$comments = App\Post::find(1)->comments()->where('title', 'foo')->first();
和hasOne
方法一樣,你還可以通過傳遞額外引數到hasMany
方法來重新設定外來鍵和本地主鍵:
return $this->hasMany('App\Comment', 'foreign_key'); return $this->hasMany('App\Comment', 'foreign_key', 'local_key'); // 在本例中,傳遞完整引數程式碼如下 return $this->hasMany('App\Comment', 'post_id', 'id');
一對多(逆向)
現在我們可以訪問文章的所有評論了,接下來讓我們定義一個關聯關係允許通過評論訪問所屬文章。要定義與hasMany
相對的關聯關係,需要在子模型中定義一個關聯方法去呼叫belongsTo
方法:
<?php namespace App; use Illuminate\Database\Eloquent\Model; class Comment extends Model{ /** * 獲取評論對應的部落格文章 */ public function post() { return $this->belongsTo('App\Post'); } }
關聯關係定義好之後,我們可以通過訪問動態屬性post
來獲取某條Comment
對應的Post
:
$comment = App\Comment::find(1); echo $comment->post->title;
在上面這個例子中,Eloquent 嘗試匹配Comment
模型的post_id
與Post
模型的id
,Eloquent 通過關聯方法名加上_id
字尾生成預設外來鍵,當然,你也可以通過傳遞自定義外來鍵名作為第二個引數傳遞到belongsTo
方法,如果你的外來鍵不是post_id
,或者你想自定義的話:
/** * 獲取評論對應的部落格文章 */ public function post(){ return $this->belongsTo('App\Post', 'foreign_key'); }
如果你的父模型不使用id
作為主鍵,或者你希望通過其他資料列來連線子模型,可以將自定義鍵名作為第三個引數傳遞給belongsTo
方法:
/** * 獲取評論對應的部落格文章 */ public function post(){ return $this->belongsTo('App\Post', 'foreign_key', 'other_key'); }
類似的,通過傳遞完整引數改寫上述呼叫程式碼如下:
return $this->belongsTo('App\Post', 'post_id', 'id');
多對多
多對多關聯比hasOne
和hasMany
關聯關係要稍微複雜一些。這種關聯關係的一個例子就是在許可權管理中,一個使用者可能有多個角色,同時一個角色可能被多個使用者共用。例如,很多使用者可能都有一個“Admin”角色。要定義這樣的關聯關係,需要三張資料表:users
、roles
和role_user
,role_user
表按照關聯模型名的字母順序命名,並且包含user_id
和role_id
兩個列。
多對多關聯通過編寫呼叫belongsToMany
方法返回結果的方式來定義,例如,我們在User
模型上定義roles
方法:
<?php namespace App; use Illuminate\Database\Eloquent\Model; class User extends Model{ /** * 使用者角色 */ public function roles() { return $this->belongsToMany('App\Role'); } }
關聯關係被定義之後,可以使用動態屬性roles
來訪問使用者的角色:
$user = App\User::find(1); foreach ($user->roles as $role) { // }
當然,和所有其它關聯關係型別一樣,你可以呼叫roles
方法來新增條件約束到關聯查詢上:
$roles = App\User::find(1)->roles()->orderBy('name')->get();
正如前面所提到的,為了確定關聯關係連線表的表名,Eloquent 以字母順序連線兩個關聯模型的名字。不過,你可以重寫這種約定 —— 通過傳遞第二個引數到belongsToMany
方法:
return $this->belongsToMany('App\Role', 'user_roles');
除了自定義連線表的表名,你還可以通過傳遞額外引數到belongsToMany
方法來自定義該表中欄位的列名。第三個引數是你定義關聯關係模型的外來鍵名稱,第四個引數你要連線到的模型的外來鍵名稱:
return $this->belongsToMany('App\Role', 'user_roles', 'user_id', 'role_id');
定義相對的關聯關係
要定義與多對多關聯相對的關聯關係,只需在關聯模型中呼叫一下belongsToMany
方法即可。我們在Role
模型中定義users
方法:
<?php namespace App; use Illuminate\Database\Eloquent\Model; class Role extends Model{ /** * 角色使用者 */ public function users() { return $this->belongsToMany('App\User'); } }
正如你所看到的,定義的關聯關係和與其對應的User
中定義的一模一樣,只是前者引用App\Role
,後者引用App\User
,由於我們再次使用了belongsToMany
方法,所有的常用表和鍵自定義選項在定義與多對多相對的關聯關係時都是可用的。
獲取中間表字段
正如你已經瞭解到的,處理多對多關聯要求一箇中間表。Eloquent 提供了一些有用的方法來與這個中間表進行互動,例如,我們假設User
物件有很多與之關聯的Role
物件,訪問這些關聯關係之後,我們可以使用這些模型上的pivot
屬性訪問中間表:
$user = App\User::find(1); foreach ($user->roles as $role) { echo $role->pivot->created_at; }
注意我們獲取到的每一個Role
模型都被自動賦上了pivot
屬性。該屬性包含一個代表中間表的模型,並且可以像其它 Eloquent 模型一樣使用。
預設情況下,只有模型主鍵才能用在pivot
物件上,如果你的pivot
表包含額外的屬性,必須在定義關聯關係時進行指定:
return $this->belongsToMany('App\Role')->withPivot('column1', 'column2');
如果你想要你的pivot
表自動包含created_at
和updated_at
時間戳,在關聯關係定義時使用withTimestamps
方法:
return $this->belongsToMany('App\Role')->withTimestamps();
自定義pivot
屬性名
上面已經提到,我們可以通過在模型上使用pivot
屬性來訪問中間表字段,此外,我們還可以在應用中自定義這個屬性名稱來提升可讀性。
例如,如果你的應用包含已經訂閱播客的使用者,那麼就會有一個使用者與播客之間的多對多關聯,在這個例子中,你可能希望將中間表訪問器改為subscription
來取代pivot
,這可以通過在定義關聯關係時使用as
方法來實現:
return $this->belongsToMany('App\Podcast') ->as('subscription') ->withTimestamps();
定義好之後,就可以使用自定義的屬性名來訪問中間表資料了:
$users = User::with('podcasts')->get(); foreach ($users->flatMap->podcasts as $podcast) { echo $podcast->subscription->created_at; }
通過中間表字段過濾關聯關係
你還可以在定義關聯關係的時候使用wherePivot
和wherePivotIn
方法過濾belongsToMany
返回的結果集:
return $this->belongsToMany('App\Role')->wherePivot('approved', 1); return $this->belongsToMany('App\Role')->wherePivotIn('priority', [1, 2]);
自定義中間表模型
如果你想要定義自定義的模型來表示關聯關係中間表,可以在定義關聯關係的時候呼叫using
方法,所有用於表示關聯關係中間表的自定義模型都必須繼承自Illuminate\Database\Eloquent\Relations\Pivot
類,用於自定義多型的多對多中間模型則繼承自Illuminate\Database\Eloquent\Relations\MorphPivot
類。例如,我們可以定義一個使用UserRole
中間模型的Role
:
<?php namespace App; use Illuminate\Database\Relations\Pivot; class Role extends Pivot { /** * The users that belong to the role. */ public function users() { return $this->belongsToMany('App\User')->using('App\UserRole'); } }
UserRole
繼承自Pivot
類:
<?php namespace App; use Illuminate\Database\Eloquent\Relations\Pivot; class UserRole extends Pivot { // }
你可以將using
和withPivot
聯合起來以便從中間表獲取欄位。例如,你可以通過傳遞列名到withPivot
方法以便從UserRole
中間表獲取created_by
和updated_by
欄位:
<?php namespace App; use Illuminate\Database\Eloquent\Model; class Role extends Model { /** * The users that belong to the role. */ public function users() { return $this->belongsToMany('App\User') ->using('App\UserRole') ->withPivot([ 'created_by', 'updated_by' ]); } }
自定義中間模型和自增ID
如果你已經定義過一個使用自定義中間模型的多對多關聯關係,並且這個中間模型有一個自增主鍵,需要確保自定義的中間模型類定義了一個被設定為true
的incrementing
屬性:
/** * Indicates if the IDs are auto-incrementing. * * @var bool */ public $incrementing = true;
遠層一對一
「遠層一對一」關聯通過單一中間關係連結模型,例如,如果每個供應商都有一個使用者,同時每個使用者都與一個使用者歷史記錄相關聯,這樣,供應商模型就可以通過使用者來訪問使用者的歷史。下面我們來看看定義這個關聯關係所需的資料表結構:
users id - integer supplier_id - integer suppliers id - integer history id - integer user_id - integer
儘管history
資料表不包含supplier_id
列,hasOneThrough
關聯仍然可以為供應商提供對使用者歷史的訪問。現在,我們已經知道了關聯關係對應的資料表結構,接下來我們在Supplier
模型上定義這個關聯:
<?php namespace App; use Illuminate\Database\Eloquent\Model; class Supplier extends Model { /** * Get the user's history. */ public function userHistory() { return $this->hasOneThrough('App\History', 'App\User'); } }
傳遞給hasOneThrough
方法的第一個引數是我們最終希望訪問的模型類名,第二個引數是中間模型的類名。
和前面幾種關聯關係一樣,在執行這個關聯查詢時也會應用常規的 Eloquent 外來鍵預設約定,如果你想要自定義關聯關係使用的外來鍵,可以將它們作為第三個和第四個引數傳遞到hasOneThrough
方法。其中,第三個引數是中間模型的外來鍵名稱,第四個引數是最終要訪問的模型的外來鍵名稱,hasOneThrough
方法還有第五個引數,預設是當前模型的主鍵名稱,以及第六個引數,表示中間模型的主鍵名稱:
class Supplier extends Model { /** * Get the user's history. */ public function userHistory() { return $this->hasOneThrough( 'App\History', 'App\User', 'supplier_id', // Foreign key on users table... 'user_id', // Foreign key on history table... 'id', // Local key on suppliers table... 'id' // Local key on users table... ); } }
遠層一對多
「遠層一對多」關聯為通過中間關聯訪問遠層的關聯關係提供了一個便捷之道。例如,Country
模型通過中間的User
模型可能擁有多個Post
模型。在這個例子中,你可以輕易的聚合給定國家的所有文章,讓我們看看定義這個關聯關係需要哪些表:
countries id - integer name - string users id - integer country_id - integer name - string posts id - integer user_id - integer title - string
儘管posts
表不包含country_id
,但是hasManyThrough
關聯提供了$country->posts
來訪問一個國家的所有文章。要執行該查詢,Eloquent 在中間表$users
上檢查country_id
,查詢到相匹配的使用者ID後,通過使用者ID來查詢posts
表。
既然我們已經查看了該關聯關係的資料表結構,接下來讓我們在Country
模型上進行定義:
<?php namespace App; use Illuminate\Database\Eloquent\Model; class Country extends Model{ /** * 獲取指定國家的所有文章 */ public function posts() { return $this->hasManyThrough('App\Post', 'App\User'); } }
第一個傳遞到hasManyThrough
方法的引數是最終我們希望訪問的模型的名稱,第二個引數是中間模型名稱。
當執行這種關聯查詢時通常 Eloquent 外來鍵規則會被使用,如果你想要自定義該關聯關係的外來鍵,可以將它們作為第三個、第四個引數傳遞給hasManyThrough
方法。第三個引數是中間模型的外來鍵名,第四個引數是最終模型的外來鍵名,第五個引數是本地主鍵。
class Country extends Model { public function posts() { return $this->hasManyThrough( 'App\Post', 'App\User', 'country_id', // users表使用的外來鍵... 'user_id', // posts表使用的外來鍵... 'id', // countries表主鍵... 'id' // users表主鍵... ); } }
多型關聯
多型關聯允許目標模型在單個關聯下歸屬於多種不同的模型。
一對一(多型)
表結構
一對一的多型關聯和簡單的一對一關聯類似,不同之處在於目標模型在單個關聯下可以歸屬於多種不同的模型。例如,Post
和User
可以共享與Image
模型的多型關聯。使用一對一多型關聯,你可以擁有一個可用於部落格文章和使用者賬戶的唯一圖片列表。首先,我們來定義表結構:
posts id - integer name - string users id - integer name - string images id - integer url - string imageable_id - integer imageable_type - string
注意images
表中的imageable_id
和imageable_type
欄位,imageable_id
欄位儲存的是文章或使用者的ID值,而imageable_type
欄位儲存的是歸屬父模型的類名。訪問imageable
關聯時,Eloquent 使用imageable_type
欄位來判定返回哪種型別的父模型(Post
還是User
)。
模型結構
接下來,我們來看看用於構建這個關聯的模型定義:
<?php namespace App; use Illuminate\Database\Eloquent\Model; class Image extends Model { /** * Get all of the owning imageable models. */ public function imageable() { return $this->morphTo(); } } class Post extends Model { /** * Get the post's image. */ public function image() { return $this->morphOne('App\Image', 'imageable'); } } class User extends Model { /** * Get the user's image. */ public function image() { return $this->morphOne('App\Image', 'imageable'); } }
獲取關聯關係
定義好資料表和模型類之後,就可以通過模型來訪問關聯關係了。例如,要獲取某篇文章的圖片,可以使用image
動態屬性:
$post = App\Post::find(1); $image = $post->image;
還可以從多型模型中通過訪問呼叫morphTo
的方法名來獲取其歸屬的父模型。在這個例子中,就是Image
模型的imageable
方法,因此,我們可以通過動態屬性的方式來訪問該方法:
$image = App\Image::find(1); $imageable = $image->imageable;
Image
模型上的imageable
關聯將會返回Post
或User
例項,這取決於哪中模型擁有該圖片。
一對多(多型)
表結構
一對多的多型關聯和簡單的一對多關聯類似,不同之處在於其目標模型可以通過單個關聯歸屬於多種模型。例如,假設應用使用者既可以對文章進行評論也可以對視訊進行評論,使用多型關聯,你可以在這兩種場景下使用單個comments
表,首先,讓我們看看構建這種關聯關係需要的表結構:
posts id - integer title - string body - text videos id - integer title - string url - string comments id - integer body - text commentable_id - integer commentable_type - string
兩個重要的需要注意的欄位是comments
表上的commentable_id
和commentable_type
。commentable_id
欄位對應Post
或Video
的 ID 值,而commentable_type
欄位對應所屬模型的類名。當訪問commentable
關聯時,ORM 根據commentable_type
欄位來判斷所屬模型的型別並返回相應模型例項。
模型結構
接下來,讓我們看看構建這種關聯關係需要在模型中定義什麼:
<?php namespace App; use Illuminate\Database\Eloquent\Model; class Comment extends Model { /** * Get all of the owning commentable models. */ public function commentable() { return $this->morphTo(); } } class Post extends Model { /** * Get all of the post's comments. */ public function comments() { return $this->morphMany('App\Comment', 'commentable'); } } class Video extends Model { /** * Get all of the video's comments. */ public function comments() { return $this->morphMany('App\Comment', 'commentable'); } }
獲取多型關聯
資料表和模型定義好以後,可以通過模型訪問關聯關係。例如,要訪問一篇文章的所有評論,可以使用動態屬性comments
:
$post = App\Post::find(1); foreach ($post->comments as $comment) { // }
你還可以通過訪問呼叫morphTo
的方法名從多型模型中獲取多型關聯的所屬物件。在本例中,就是Comment
模型中的commentable
方法。因此,我們可以用動態屬性的方式訪問該方法:
$comment = App\Comment::find(1); $commentable = $comment->commentable;
Comment
模型的commentable
關聯返回Post
或Video
例項,這取決於哪個型別的模型擁有該評論。
多對多(多型)
表結構
多對多的多型關聯比morphOne
和morphMany
關聯稍微複雜一些。例如,一個部落格的Post
和Video
模型可能共享一個Tag
模型的多型關聯。使用對多對的多型關聯允許你在部落格文章和視訊之間有唯一的標籤列表。首先,讓我們看看錶結構:
posts id - integer name - string videos id - integer name - string tags id - integer name - string taggables tag_id - integer taggable_id - integer taggable_type - string
模型結構
接下來,我們準備在模型中定義該關聯關係。Post
和Video
模型都有一個tags
方法呼叫 Eloquent 基類的morphToMany
方法:
<?php namespace App; use Illuminate\Database\Eloquent\Model; class Post extends Model { /** * 獲取指定文章所有標籤 */ public function tags() { return $this->morphToMany('App\Tag', 'taggable'); } }
定義相對的關聯關係
接下來,在Tag
模型中,應該為每一個關聯模型定義一個方法,例如,我們定義一個posts
方法和videos
方法:
<?php namespace App; use Illuminate\Database\Eloquent\Model; class Tag extends Model { /** * 獲取所有分配該標籤的文章 */ public function posts() { return $this->morphedByMany('App\Post', 'taggable'); } /** * 獲取分配該標籤的所有視訊 */ public function videos() { return $this->morphedByMany('App\Video', 'taggable'); } }
獲取關聯關係
定義好資料庫和模型後可以通過模型訪問關聯關係。例如,要訪問一篇文章的所有標籤,可以使用動態屬性tags
:
$post = App\Post::find(1); foreach ($post->tags as $tag) { // }
還可以通過訪問呼叫morphedByMany
的方法名從多型模型中獲取多型關聯的所屬物件。在本例中,就是Tag
模型中的posts
或者videos
方法:
$tag = App\Tag::find(1); foreach ($tag->videos as $video) { // }
自定義多型型別
預設情況下,Laravel 使用完全限定類名(包含名稱空間的完整類名)來儲存關聯模型的型別。舉個例子,上面示例中的Comment
可能屬於某個Post
或Video
,預設的commentable_type
可能是App\Post
或App\Video
。不過,有時候你可能需要解除資料庫和應用內部結構之間的耦合,這樣的情況下,可以定義一個morphMap
關聯來告知 Eloquent 為每個模型使用自定義名稱替代完整類名:
use Illuminate\Database\Eloquent\Relations\Relation; Relation::morphMap([ 'posts' => 'App\Post', 'videos' => 'App\Video', ]);
你可以在AppServiceProvider
的boot
方法中註冊這個morphMap
,如果需要的話,也可以建立一個獨立的服務提供者來實現這一功能。
關聯查詢
由於 Eloquent 所有關聯關係都是通過方法定義,你可以呼叫這些方法來獲取關聯關係的例項而不需要再去手動執行關聯查詢。此外,所有 Eloquent 關聯關係型別同時也是查詢構建器,允許你在最終資料庫執行 SQL 之前繼續新增條件約束到關聯查詢上。
例如,假定在一個部落格系統中一個User
模型有很多相關的Post
模型:
<?php namespace App; use Illuminate\Database\Eloquent\Model; class User extends Model{ /** * 獲取指定使用者的所有文章 */ public function posts() { return $this->hasMany('App\Post'); } }
你可以像這樣查詢posts
關聯並新增額外的條件約束到該關聯關係上:
$user = App\User::find(1); $user->posts()->where('active', 1)->get();
你可以在關聯關係上使用任何查詢構建器提供任何的方法!所以,掌握查詢構建器的使用是掌握所有 Laravel 資料庫操作的重要基石。
關聯方法 Vs. 動態屬性
如果你不需要新增額外的條件約束到 Eloquent 關聯查詢,你可以簡單通過動態屬性來訪問關聯物件,例如,還是拿User
和Post
模型作為例子,你可以像這樣訪問使用者的所有文章:
$user = App\User::find(1); foreach ($user->posts as $post) { // }
動態屬性是“懶惰式載入”,意味著當你真正訪問它們的時候才會載入關聯資料。正因為如此,開發者經常使用渴求式載入 來預載入他們知道在載入模型時要被訪問的關聯關係。渴求式載入有效減少了必須要被執行用以載入模型關聯的 SQL 查詢。
查詢存在的關聯關係
訪問一個模型的記錄的時候,你可能希望基於關聯關係是否存在來限制查詢結果的數目。例如,假設你想要獲取所有至少有一個評論的部落格文章,要實現這個功能,可以傳遞關聯關係的名稱到has
和orHas
方法:
// 獲取所有至少有一條評論的文章... $posts = App\Post::has('comments')->get();
你還可以指定操作符和數目來自定義查詢:
// 獲取所有至少有三條評論的文章... $posts = Post::has('comments', '>=', 3)->get();
還可以使用”.“來構造巢狀has
語句,例如,你要獲取所有至少有一條評論及投票的文章:
// 獲取所有至少有一條評論獲得投票的文章... $posts = Post::has('comments.votes')->get();
如果你需要更強大的功能,可以使用whereHas
和orWhereHas
方法將「where」條件放到has
查詢上,這些方法允許你新增自定義條件約束到關聯關係條件約束,例如檢查一條評論的內容:
use Illuminate\Database\Eloquent\Builder; // Retrieve posts with at least one comment containing words like foo%... $posts = App\Post::whereHas('comments', function ($query) { $query->where('content', 'like', 'foo%'); })->get(); // Retrieve posts with at least ten comments containing words like foo%... $posts = App\Post::whereHas('comments', function ($query) { $query->where('content', 'like', 'foo%'); }, '>=', 10)->get();
無關聯結果查詢
訪問一個模型的記錄時,你可能需要基於缺失關聯關係的模型對查詢結果進行限定。例如,假設你想要獲取所有沒有評論的部落格文章,可以傳遞關聯關係名稱到doesntHave
和orDoesntHave
方法來實現:
$posts = App\Post::doesntHave('comments')->get();
如果你需要更多功能,可以使用whereDoesntHave
和orWhereDoesntHave
方法新增更多「where」條件到doesntHave
查詢,這些方法允許你新增自定義約束條件到關聯關係約束,例如檢查評論內容:
use Illuminate\Database\Eloquent\Builder; $posts = App\Post::whereDoesntHave('comments', function (Builder $query) { $query->where('content', 'like', 'foo%'); })->get();
還可以使用「.」號查詢巢狀的關聯關係,例如,下面的查詢會從有效作者那裡獲取所有帶評論的文章:
use Illuminate\Database\Eloquent\Builder; $posts = App\Post::whereDoesntHave('comments.author', function (Builder $query) { $query->where('banned', 1); })->get();
統計關聯模型
如果你想要在不載入關聯關係的情況下統計關聯結果數目,可以使用withCount
方法,該方法會放置一個{relation}_count
欄位到結果模型。例如:
$posts = App\Post::withCount('comments')->get(); foreach ($posts as $post) { echo $post->comments_count; }
你可以像新增約束條件到查詢一樣來新增多個關聯關係的「計數」:
$posts = Post::withCount(['votes', 'comments' => function ($query) { $query->where('content', 'like', 'foo%'); }])->get(); echo $posts[0]->votes_count; echo $posts[0]->comments_count;
還可以為關聯關係計數結果設定別名,從而允許在一個關聯關係上進行多維度計數:
$posts = App\Post::withCount([ 'comments', 'comments as pending_comments' => function ($query) { $query->where('approved', false); } ])->get(); echo $posts[0]->comments_count; echo $posts[0]->pending_comments_count;
如果你將withCount
和select
語句組合起來使用,需要在select
方法之後呼叫withCount
:
$posts = App\Post::select(['title', 'body'])->withCount('comments'); echo $posts[0]->title; echo $posts[0]->body; echo $posts[0]->comments_count;
渴求式載入
當以屬性方式訪問 Eloquent 關聯關係的時候,關聯關係資料是「懶惰式載入」的,這意味著關聯關係資料直到第一次訪問的時候才被載入。不過,Eloquent 還可以在查詢父級模型的同時「渴求式載入」關聯關係。渴求式載入緩解 N+1 查詢問題,要闡明 N+1 查詢問題,檢視關聯到Author
的Book
模型:
<?php namespace App; use Illuminate\Database\Eloquent\Model; class Book extends Model { /** * 獲取寫這本書的作者 */ public function author() { return $this->belongsTo('App\Author'); } }
現在,讓我們獲取所有書及其作者:
$books = App\Book::all(); foreach ($books as $book) { echo $book->author->name; }
該迴圈先執行 1 次查詢獲取表中的所有書,然後另一個查詢獲取每一本書的作者,因此,如果有25本書,要執行26次查詢:1次是獲取書本身,剩下的25次查詢是為每一本書獲取其作者。
謝天謝地,我們可以使用渴求式載入來減少該操作到 2 次查詢。當查詢的時候,可以使用with
方法指定應該被渴求式載入的關聯關係:
$books = App\Book::with('author')->get(); foreach ($books as $book) { echo $book->author->name; }
在該操作中,只執行兩次查詢即可:
select * from books select * from authors where id in (1, 2, 3, 4, 5, ...)
渴求式載入多個關聯關係
有時候你需要在單個操作中渴求式載入多個不同的關聯關係。要實現這個功能,只需要新增額外的引數到with
方法即可:
$books = App\Book::with('author', 'publisher')->get();
巢狀的渴求式載入
要渴求式載入巢狀的關聯關係,可以使用”.“語法。例如,我們在一個 Eloquent 語句中渴求式載入所有書的作者及所有作者的個人聯絡方式:
$books = App\Book::with('author.contacts')->get();
渴求式載入指定欄位
並不是每次獲取關聯關係時都需要所有欄位,因此,Eloquent 允許你在關聯查詢時指定要查詢的欄位:
$users = App\Book::with('author:id,name')->get();
注:使用這個特性時,id
欄位是必須列出的。
帶條件約束的渴求式載入
有時候我們希望渴求式載入一個關聯關係,但還想為渴求式載入指定更多的查詢條件:
$users = App\User::with(['posts' => function ($query) { $query->where('title', 'like', '%first%'); }])->get();
在這個例子中,Eloquent 只渴求式載入title
包含first
的文章。當然,你還可以呼叫其它查詢構建器來自定義渴求式載入操作:
$users = App\User::with(['posts' => function ($query) { $query->orderBy('created_at', 'desc'); }])->get();
注:查詢構建器方法limit
和take
不能在渴求式載入中使用。
懶惰渴求式載入
有時候你需要在父模型已經被獲取後渴求式載入一個關聯關係。例如,這在你需要動態決定是否載入關聯模型時可能很有用:
$books = App\Book::all(); if ($someCondition) { $books->load('author', 'publisher'); }
如果你需要設定更多的查詢條件到渴求式載入查詢上,可以傳遞一個包含你想要記載的關聯關係陣列到load
方法,陣列的值應該是接收查詢例項的閉包:
$books->load(['author' => function ($query) { $query->orderBy('published_date', 'asc'); }]);
如果想要在關係管理尚未被載入的情況下載入它,可以使用loadMissing
方法:
public function format(Book $book) { $book->loadMissing('author'); return [ 'name' => $book->name, 'author' => $book->author->name ]; }
巢狀的懶惰渴求式載入 &morphTo
如果你想要渴求式載入一個morphTo
關聯,以及該關聯可能返回的各種實體巢狀關聯,可以使用loadMorph
方法。
該方法接收morphTo
關聯名稱作為第一個引數,以及一個模型/關聯對陣列作為第二個引數。我們通過一個示例來說明該方法的使用:
<?php use Illuminate\Database\Eloquent\Model; class ActivityFeed extends Model { /** * Get the parent of the activity feed record. */ public function parentable() { return $this->morphTo(); } }
在這個例子中,我們假設Event
、Photo
和Post
模型可以建立ActivityFeed
模型。此外,還假設Event
模型歸屬於Calendar
模型,Photo
模型與Tag
模型相關聯,並且Post
模型歸屬於Author
模型。
使用這些模型定義和關聯,我們可以獲取ActivityFeed
模型例項並渴求式載入所有的parentable
模型及其各自巢狀的關聯關係:
$activities = ActivityFeed::with('parentable') ->get() ->loadMorph('parentable', [ Event::class => ['calendar'], Photo::class => ['tags'], Post::class => ['author'], ]);
插入 & 更新關聯模型
save 方法
Eloquent 為新增新模型到關聯關係提供了便捷方法。例如,如果你需要插入新的Comment
到Post
模型,可以從關聯關係的save
方法直接插入Comment
而不是手動設定Comment
的post_id
屬性:
$comment = new App\Comment(['message' => 'A new comment.']); $post = App\Post::find(1); $post->comments()->save($comment);
注意我們沒有用動態屬性方式訪問comments
,而是呼叫comments
方法獲取關聯關係例項。save
方法會自動新增post_id
值到新的Comment
模型。
如果你需要儲存多個關聯模型,可以使用saveMany
方法:
$post = App\Post::find(1); $post->comments()->saveMany([ new App\Comment(['message' => 'A new comment.']), new App\Comment(['message' => 'Another comment.']), ]);
遞迴儲存模型&關聯關係
如果你想要save
模型及其所有相關的關聯關係,可以使用push
方法:
$post = App\Post::find(1); $post->comments[0]->message = 'Message'; $post->comments[0]->author->name = 'Author Name'; $post->push();
create 方法
除了save
和saveMany
方法外,還可以使用create
方法,該方法接收屬性陣列、建立模型、然後插入資料庫。save
和create
的不同之處在於save
接收整個 Eloquent 模型例項而create
接收原生 PHP 陣列:
$post = App\Post::find(1); $comment = $post->comments()->create([ 'message' => 'A new comment.', ]);
注:使用create
方法之前確保先瀏覽屬性批量賦值文件。
還可以使用createMany
方法來建立多個關聯模型:
$post = App\Post::find(1); $post->comments()->createMany([ [ 'message' => 'A new comment.', ], [ 'message' => 'Another new comment.', ], ]);
還可以使用findOrNew
、firstOrNew
、firstOrCreate
和updateOrCreate
方法來建立和更新關聯模型。
從屬關聯關係
更新belongsTo
關聯的時候,可以使用associate
方法,該方法會在子模型設定外來鍵:
$account = App\Account::find(10); $user->account()->associate($account); $user->save();
移除belongsTo
關聯的時候,可以使用dissociate
方法。該方法會設定關聯關係的外來鍵為null
:
$user->account()->dissociate(); $user->save();
預設模型
belongsTo
關聯關係允許你在給定關聯關係為null
的情況下定義一個預設的返回模型,我們將這種模式稱之為空物件模式,使用這種模式的好處是不用在程式碼中編寫大量的判斷檢查邏輯。在下面的例子中,user
關聯將會在沒有使用者與文章關聯的情況下返回一個空的App\User
模型:
/** * 獲取文章作者 */ public function user() { return $this->belongsTo('App\User')->withDefault(); }
要通過屬性填充預設的模型,可以傳遞資料或閉包到withDefault
方法:
/** * 獲取文章作者 */ public function user() { return $this->belongsTo('App\User')->withDefault([ 'name' => 'Guest Author', ]); } /** * 獲取文章作者 */ public function user() { return $this->belongsTo('App\User')->withDefault(function ($user) { $user->name = 'Guest Author'; }); }
多對多關聯
附加/分離
處理多對多關聯的時候,Eloquent 還提供了一些額外的輔助函式使得處理關聯模型變得更加方便。例如,我們假定一個使用者可能有多個角色,同時一個角色屬於多個使用者,要通過在連線模型的中間表中插入記錄附加角色到使用者上,可以使用attach
方法:
$user = App\User::find(1); $user->roles()->attach($roleId);
附加關聯關係到模型,還可以以陣列形式傳遞額外被插入資料到中間表:
$user->roles()->attach($roleId, ['expires' => $expires]);
當然,有時候有必要從使用者中移除角色,要移除一個多對多關聯記錄,使用detach
方法。detach
方法將會從中間表中移除相應的記錄;但是,兩個模型在資料庫中都保持不變:
// 從指定使用者中移除角色... $user->roles()->detach($roleId); // 從指定使用者移除所有角色... $user->roles()->detach();
為了方便,attach
和detach
還接收陣列形式的 ID 作為輸入:
$user = App\User::find(1); $user->roles()->detach([1, 2, 3]); $user->roles()->attach([ 1 => ['expires' => $expires], 2 => ['expires' => $expires] ]);
同步關聯
你還可以使用sync
方法構建多對多關聯。sync
方法接收陣列形式的 ID 並將其放置到中間表。任何不在該陣列中的 ID 對應記錄將會從中間表中移除。因此,該操作完成後,只有在陣列中的 ID 對應記錄還存在於中間表:
$user->roles()->sync([1, 2, 3]);
你還可以和 ID 一起傳遞額外的中間表值:
$user->roles()->sync([1 => ['expires' => true], 2, 3]);
如果你不想要刪除已存在的ID,可以使用syncWithoutDetaching
方法:
$user->roles()->syncWithoutDetaching([1, 2, 3]);
切換關聯
多對多關聯還提供了一個toggle
方法用於切換給定 ID 的附加狀態,如果給定ID當前被附加,則取消附加,類似的,如果當前沒有附加,則附加:
$user->roles()->toggle([1, 2, 3]);
在中間表上儲存額外資料
處理多對多關聯時,save
方法接收額外中間表屬性陣列作為第二個引數:
App\User::find(1)->roles()->save($role, ['expires' => $expires]);
更新中間表記錄
如果你需要更新中間表中已存在的行,可以使用updateExistingPivot
方法。該方法接收中間記錄外來鍵和屬性陣列進行更新:
$user = App\User::find(1); $user->roles()->updateExistingPivot($roleId, $attributes);
觸發父模型時間戳更新
當一個模型屬於另外一個時,例如Comment
屬於Post
,子模型更新時父模型的時間戳也被更新將很有用,例如,當Comment
模型被更新時,你可能想要”觸發“更新其所屬模型Post
的updated_at
時間戳。Eloquent 使得這項操作變得簡單,只需要新增包含關聯關係名稱的touches
屬性到子模型即可:
<?php namespace App; use Illuminate\Database\Eloquent\Model; class Comment extends Model{ /** * 要觸發的所有關聯關係 * * @var array */ protected $touches = ['post']; /** * 評論所屬文章 */ public function post() { return $this->belongsTo('App\Post'); } }
現在,當你更新Comment
時,所屬模型Post
將也會更新其updated_at
值,從而方便得知何時更新Post
模型快取:
$comment = App\Comment::find(1); $comment->text = 'Edit to this comment!'; $comment->save();