模型關聯
Eloquent: 關聯
更新Belongs To
關聯多對多重關聯定義關聯
Eloquent 關聯在 Eloquent 模型類別中以方法的形式呈現。如同 Eloquent 模型本身,關聯也可以作為強大的 查詢語句建構器 使用,提供了強大的鍊式呼叫和查詢功能。例如,我們可以在 posts
關聯的鍊式呼叫中附加一個約束條件:
$user->posts()->where('active', 1)->get();
不過在深入使用關聯之前,讓我們先學習如何定義每個關聯類型。
一對一
#一對一是最基本的關聯關係。例如,一個 User
模型可能會關聯一個 Phone
模型。為了定義這個關聯,我們要在 User
模型中寫一個 phone
方法。在phone
方法內部呼叫hasOne
方法並傳回其結果:
<?php namespace App; use Illuminate\Database\Eloquent\Model; class User extends Model{ /** * Get the phone record associated with the user. */ 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 將會在 Phone 記錄的 user_id 欄位中尋找與使用者表的 id 欄位相符的值。如果您希望該關聯使用id 以外的自訂鍵名,則可以為hasOne 方法傳遞第三個參數:
return $this->hasOne('App\Phone', 'foreign_key', 'local_key');
定義反向關聯
我們已經能從User
模型存取到Phone
模型了。現在,讓我們再在 Phone
模型上定義一個關聯,這個關聯能讓我們存取到擁有該電話的 User
模型。我們可以使用與hasOne
方法對應的belongsTo
方法來定義反向關聯:
<?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
模型上的id
。它是透過檢查關係方法的名稱並使用 _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'); }#
一對多
『一對多』關聯用於定義單一模型擁有任意數量的其它關聯模型。例如,一篇部落格文章可能會有無限多條評論。正如其它所有的Eloquent 關聯一樣,一對多關聯的定義也是在Eloquent 模型中寫一個方法:
<?php namespace App; use Illuminate\Database\Eloquent\Model; class Post extends Model{ /** * 获取博客文章的评论 */ public function comments() { return $this->hasMany('App\Comment'); } }
記住一點,Eloquent 將會自動決定Comment
模型的外鍵屬性。依照約定,Eloquent 將會使用所屬模型名稱的 『snake case』形式,再加上 _id
後綴作為外鍵欄位。因此,在上面這個範例中,Eloquent 將假定 Comment
對應到 Post
模型上的外鍵就是 post_id
。
一旦關係被定義好以後,就可以透過造訪 Post
模型的 comments
屬性來取得評論的集合。記住,由於Eloquent 提供了『動態屬性』 ,所以我們可以像存取模型的屬性一樣存取關聯方法:
$comments = App\Post::find(1)->comments;foreach ($comments as $comment) { //}
當然,由於所有的關聯也可以作為查詢語句建構器使用,因此你可以使用鍊式呼叫的方式,在comments
方法上加入額外的約束條件:
$comment = 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');
一對多(反向)
現在,我們已經能獲得一篇文章的所有評論,接著再定義一個透過評論獲得所屬文章的關聯關係。這個關聯是hasMany
關聯的反向關聯,需要在子級模型中使用belongsTo
方法定義它:
<?php namespace App; use Illuminate\Database\Eloquent\Model; class Comment extends Model{ /** * 获取此评论所属文章 */ public function post() { return $this->belongsTo('App\Post'); } }
這個關係定義好以後,我們就可以透過存取Comment
模型的post
這個『動態屬性』來取得關聯的Post
模型了:
$comment = App\Comment::find(1); echo $comment->post->title;
在上面的範例中, Eloquent 會嘗試以Comment
模型的post_id
與Post
模型的id
進行比對。預設外鍵名是 Eloquent 依據關聯名,並在關聯名後加上 _ 再加上主鍵欄位名稱作為後綴確定的。當然,如果Comment
模型的外鍵不是post_id
,那麼可以將自訂鍵名稱作為第二個參數傳遞給belongsTo
方法:
/** * 获取此评论所属文章 */ public function post(){ return $this->belongsTo('App\Post', 'foreign_key'); }
如果父級模型沒有使用id
作為主鍵,或者是希望用不同的欄位來連接子級模型,則可以透過給belongsTo
方法傳遞第三個參數的形式指定父級資料表的自訂鍵:
/** * 获取此评论所属文章 */ public function post(){ return $this->belongsTo('App\Post', 'foreign_key', 'other_key'); }#
多對多
多對多重關聯比 hasOne
和 hasMany
關聯稍微複雜些。舉個例子,一個使用者可以擁有很多種角色,同時這些角色也被其他使用者分享。例如,許多使用者可能都有 “管理員” 這個角色。要定義這種關聯,需要三個資料庫表: 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', 'role_user');
除了自訂連接表的表名,你還可以透過傳遞額外的參數到belongsToMany
方法來定義該表中欄位的鍵名。第三個參數是定義此關聯的模型在連接表裡的外鍵名,第四個參數是另一個模型在連接表裡的外鍵名:
return $this->belongsToMany('App\Role', 'role_user', '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'); } }
如你所見,除了引入模型為App\User
外,其它與在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
物件只包含兩個關聯模型的主鍵,如果你的中間表裡還有其他額外字段,你必須在定義關聯時明確指出:
return $this->belongsToMany('App\Role')->withPivot('column1', 'column2');
如果你想讓中間表自動維護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 類別。例如,
我們在寫Role 模型的關聯時,使用自訂中間表模型UserRole
<?php namespace App; use Illuminate\Database\Eloquent\Model; class Role extends Model{ /** * 拥有此角色的所有用户 */ 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{ /** * 拥有此角色的用户。 */ public function users() { return $this->belongsToMany('App\User') ->using('App\UserRole') ->withPivot([ 'created_by', 'updated_by' ]); } }
帶有遞增ID 的自訂中繼模型
#如果你用一個自訂的中繼模型定義了多對多的關係,而且這個中繼模型擁有一個自增的主鍵,你應確保這個自訂中繼模型類別中定義了一個incrementing
屬性其值為true
:
/** * 标识 ID 是否自增。 * * @var bool */ public $incrementing = true;
遠端一對一關係
遠端一對一關聯透過一個中間關聯模型實現。
例如,如果每個供應商都有一個用戶,並且每個用戶與一個用戶歷史記錄相關聯,那麼供應商可以透過用戶訪問用戶的歷史記錄,讓我們看看定義這種關係所需的資料庫表:
suppliers id - integer users id - integer supplier_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{ /** * 用户的历史记录。 */ public function userHistory() { return $this->hasOneThrough('App\History', 'App\User'); } }
傳遞給hasOneThrough
方法的第一個參數是希望訪問的模型名稱,第二個參數是中間模型的名稱。
當執行關聯查詢時,通常會使用 Eloquent 約定的外鍵名。如果你想要自訂關聯的鍵,可以透過給hasOneThrough
方法傳遞第三個和第四個參數實現,第三個參數表示中間模型的外鍵名,第四個參數表示最終模型的外鍵名。第五個參數表示本地鍵名,而第六個參數表示中間模型的本地鍵名:
class Supplier extends Model{ /** * 用户的历史记录。 */ public function userHistory() { return $this->hasOneThrough( 'App\History', 'App\User', 'supplier_id', // 用户表外键 'user_id', // 历史记录表外键 'id', // 供应商本地键 'id' // 用户本地键 ); } }
遠端一對多關聯
遠端一對多關聯提供了方便、簡短的方式透過中間的關聯來獲得遠層的關聯。例如,一個 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', // 用户表外键 'user_id', // 文章表外键 'id', // 国家表本地键 'id' // 用户表本地键 ); } }
多態關聯
多態關聯允許目標模型借助單一關聯從屬於多個模型。
#一對一(多態)
表結構
#一對一多態關聯與簡單的一對一關聯類似;不過,目標模型能夠在一個關聯上從屬於多個模型。例如,部落格 Post
和 User
可能共用一個關聯到 Image
模型的關係。使用一對一多態關聯允許使用一個唯一圖片清單同時用於部落格文章和使用者帳戶。讓我們先來看看表格結構:
posts id - integer name - string users id - integer name - string images id - integer url - string imageable_id - integer imageable_type - string
要特別留意 images
表的 imageable_id
和 imageable_type
欄位。 imageable_id
欄位包含文章或使用者的 ID 值,而 imageable_type
欄位包含的則是父模型的類別名稱。 Eloquent 在存取 imageable
時使用 imageable_type
欄位來判斷父模型的 「型別」。
模型結構
接下來,再看看建立關聯的模型定義:
<?php namespace App; use Illuminate\Database\Eloquent\Model; class Image extends Model{ /** * 获取拥有此图片的模型。 */ public function imageable() { return $this->morphTo(); } } class Post extends Model{ /** * 获取文章图片。 */ public function image() { return $this->morphOne('App\Image', 'imageable'); } } class User extends Model{ /** * 获取用户图片。 */ 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
模型結構
接下來,看看建構這個關聯的模型定義:
<?php namespace App; use Illuminate\Database\Eloquent\Model; class Comment extends Model{ /** * 获取拥有此评论的模型。 */ public function commentable() { return $this->morphTo(); } } class Post extends Model{ /** * 获取此文章的所有评论。 */ public function comments() { return $this->morphMany('App\Comment', 'commentable'); } } class Video extends Model{ /** * 获取此视频的所有评论。 */ 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
模型都有呼叫Eloquent 基底類別上 morphToMany
方法的tags
方法:
<?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) {
//
}
方法呼叫的方法名稱來從多型模型取得其所屬模型。在這個範例中,就是 Tag
模型的 posts
或 videos
方法。可以像動態屬性一樣存取這些方法:$tag = App\Tag::find(1);foreach ($tag->videos as $video) {
//
}
預設情況下,Laravel使用完全限定類別名稱儲存關聯模型類型。在上面的一對多範例中, 因為
Comment 可能從屬於一個Post
或一個Video
,預設的commentable_type
就將分別是App\Post
或App\Video
。不過,你可能希望資料庫與應用程式的內部結構解耦。在這種情況下,可以定義一個「morph 映射」 來通知Eloquent 使用自訂名稱取代對應的類別名稱:
可以在use Illuminate\Database\Eloquent\Relations\Relation;
Relation::morphMap([
'posts' => 'App\Post',
'videos' => 'App\Video',
]);
的 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');
}
}
關聯,並為其添加額外的約束:
你可以在關聯上使用任意查詢建構器方法,請查閱查詢建構器文檔,學習那些對你有用的方法。 $user = App\User::find(1);
$user->posts()->where('active', 1)->get();
关联方法 Vs. 动态属性
如果不需要向 Eloquent 关联查询添加额外的约束,可以像属性一样访问关联。例如,继续使用 User
和 Post
示例模型,可以这样访问用户的全部文章:
$user = App\User::find(1); foreach ($user->posts as $post) { // }
动态属性是「懒加载」的,这意味着它们仅在你真实访问关联数据时才被载入。因此,开发者经常使用 预加载 预先加载那些他们确知在载入模型后将访问的关联。对载入模型关联中必定被执行的 SQL 查询而言,预加载显著减少了查询的执行次数。
查询已存在的关联
在访问模型记录时,可能希望基于关联的存在限制查询结果。比如想要获取至少存在一条评论的所有文章,可以通过给 has
和 orHas
方法传递关联名称来实现:
// 获取至少存在一条评论的所有文章... $posts = App\Post::has('comments')->get();
还可以指定运算符和数量进一步自定义查询:
// 获取评论超过三条的文章... $posts = App\Post::has('comments', '>=', 3)->get();
还可以用 「点」语法构造嵌套的 has
语句。比如,可以获取拥有至少一条评论和投票的文章:
// 获取拥有至少一条带有投票评论的文章... $posts = App\Post::has('comments.votes')->get();
如果需要更多功能,可以使用 whereHas
和 orWhereHas
方法将「where」 条件放到 has
查询上。这些方法允许你向关联加入自定义约束,比如检查评论内容:
use Illuminate\Database\Eloquent\Builder; // 获取至少带有一条评论内容包含 foo% 关键词的文章... $posts = App\Post::whereHas('comments', function ($query) { $query->where('content', 'like', 'foo%');})->get(); // 获取至少带有十条评论内容包含 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 = App\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_count' => function ($query) { $query->where('approved', false); }])->get(); echo $posts[0]->comments_count; echo $posts[0]->pending_comments_count;
如果將withCount
和select
# 查詢組裝在一起,請確保在select
方法之後呼叫withCount
:
$query = 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 查詢問題,考慮 Book
模型關聯到Author
的情況:
<?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; }
此循環將執行一個查詢,用於取得全部書籍,然後為每本書執行取得作者的查詢。如果我們有 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();
##{note} 在使用這個特性時,一定要在要取得的列的清單中包含
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();
{note} 在約束預先載入時,不能使用
limit
和take
查詢建構器方法。
預先載入
有可能你還希望在模型載入完成後在進行渴求式加載。舉例來說,如果你想要動態的載入關聯數據,那麼load
方法對你來說會非常有用:
$books = App\Book::all(); if ($someCondition) { $books->load('author', 'publisher'); }
如果你想要在渴求式載入的查詢語句中進行條件約束,你可以透過陣列的形式去加載,鍵為對應的關聯關係,值為Closure
閉包函數,該閉包的參數為一個query
實例:
$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'], ]);插入& 更新關聯模型
儲存方法
Eloquent 為新模型新增關聯提供了便捷的方法。例如,也許你需要新增一個新的 Comment
到一個 Post
模型。你不用在Comment
中手動設定post_id
屬性,就可以直接使用關聯模型的save
方法將Comment
直接插入:
$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();新增方法除了
save 和
saveMany 方法外,你還可以使用
create 方法。它接受一個屬性數組,同時會建立模型並插入到資料庫中。還有,
save 方法和
create 方法的不同之處在於,
save 方法接受一個完整的Eloquent 模型實例,而
create則接受普通的PHP 陣列:
$post = App\Post::find(1); $comment = $post->comments()->create([ 'message' => 'A new comment.', ]);
{tip} 在使用你也可以使用create
方法前,請務必確保查看過本文檔的批次賦值章節。
createMany 方法來建立多個關聯模型:
$post = App\Post::find(1);$post->comments()->createMany([ [ 'message' => 'A new comment.', ], [ 'message' => 'Another new comment.', ], ]);你也可以使用
findOrNew、
firstOrNew、
firstOrCreate 和
updateOrCreate 方法來建立和更新關係模型.
belongsTo 關聯
當更新belongsTo 關聯時,可以使用
associate 方法。此方法將會在子模型中設定外鍵:
$account = App\Account::find(10); $user->account()->associate($account);$user->save();當移除
belongsTo 關聯時,可以使用
dissociate 方法。此方法會將關聯外鍵設為
null:
$user->account()->dissociate();$user->save();
預設模型
belongsTo
關係允許你指定預設模型,當給定關係為 null
時,將會傳回預設模型。這個模式被稱為 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
方法將會移除中間表對應的記錄;但是這2 個模型都會保留在資料庫中:
// 移除用户的一个角色... $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 已被附加在中間表中,那麼它將會被移除,同樣,如果如果給定的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);
更新父級時間戳記
當一個模型屬belongsTo
或belongsToMany
另一個模型時, 例如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();