模型關(guān)聯(lián)
模型關(guān)聯(lián)
Eloquent: 關(guān)聯(lián)
簡介
數(shù)據(jù)庫表通常相互關(guān)聯(lián)。 例如,一篇博客文章可能有許多評論,或者一個(gè)訂單對應(yīng)一個(gè)下單用戶。Eloquent 讓這些關(guān)聯(lián)的管理和使用變得簡單,并支持多種類型的關(guān)聯(lián):
定義關(guān)聯(lián)
Eloquent 關(guān)聯(lián)在 Eloquent 模型類中以方法的形式呈現(xiàn)。如同 Eloquent 模型本身,關(guān)聯(lián)也可以作為強(qiáng)大的 查詢語句構(gòu)造器 使用,提供了強(qiáng)大的鏈?zhǔn)秸{(diào)用和查詢功能。例如,我們可以在 posts
關(guān)聯(lián)的鏈?zhǔn)秸{(diào)用中附加一個(gè)約束條件:
$user->posts()->where('active', 1)->get();
不過在深入使用關(guān)聯(lián)之前,讓我們先學(xué)習(xí)如何定義每種關(guān)聯(lián)類型。
一對一
一對一是最基本的關(guān)聯(lián)關(guān)系。例如,一個(gè) User
模型可能關(guān)聯(lián)一個(gè) Phone
模型。為了定義這個(gè)關(guān)聯(lián),我們要在 User
模型中寫一個(gè) phone
方法。在 phone
方法內(nèi)部調(diào)用 hasOne
方法并返回其結(jié)果:
<?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
方法的第一個(gè)參數(shù)是關(guān)聯(lián)模型的類名。一旦定義了模型關(guān)聯(lián),我們就可以使用 Eloquent 動(dòng)態(tài)屬性獲得相關(guān)的記錄。動(dòng)態(tài)屬性允許你訪問關(guān)系方法就像訪問模型中定義的屬性一樣:
$phone = User::find(1)->phone;
Eloquent 會基于模型名決定外鍵名稱。在這種情況下,會自動(dòng)假設(shè) Phone
模型有一個(gè) user_id
外鍵。如果你想覆蓋這個(gè)約定,可以傳遞第二個(gè)參數(shù)給 hasOne
方法:
return $this->hasOne('App\Phone', 'foreign_key');
另外,Eloquent 假設(shè)外鍵的值是與父級 id (或自定義 $primaryKey) 列的值相匹配的。換句話說,Eloquent 將會在 Phone 記錄的 user_id 列中查找與用戶表的 id 列相匹配的值。如果您希望該關(guān)聯(lián)使用 id 以外的自定義鍵名,則可以給 hasOne 方法傳遞第三個(gè)參數(shù):
return $this->hasOne('App\Phone', 'foreign_key', 'local_key');
定義反向關(guān)聯(lián)
我們已經(jīng)能從 User
模型訪問到 Phone
模型了?,F(xiàn)在,讓我們再在 Phone
模型上定義一個(gè)關(guān)聯(lián),這個(gè)關(guān)聯(lián)能讓我們訪問到擁有該電話的 User
模型。我們可以使用與 hasOne
方法對應(yīng)的 belongsTo
方法來定義反向關(guān)聯(lián):
<?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
。它是通過檢查關(guān)系方法的名稱并使用 _id
作為后綴名來確定默認(rèn)外鍵名稱的。但是,如果 Phone
模型的外鍵不是 user_id
,那么可以將自定義鍵名作為第二個(gè)參數(shù)傳遞給 belongsTo
方法:
/** * 獲得擁有此電話的用戶 */ public function user(){ return $this->belongsTo('App\User', 'foreign_key'); }
如果父級模型沒有使用 id
作為主鍵,或者是希望用不同的字段來連接子級模型,則可以通過給 belongsTo
方法傳遞第三個(gè)參數(shù)的形式指定父級數(shù)據(jù)表的自定義鍵:
/** * 獲得擁有此電話的用戶 */ public function user(){ return $this->belongsTo('App\User', 'foreign_key', 'other_key'); }
一對多
『一對多』關(guān)聯(lián)用于定義單個(gè)模型擁有任意數(shù)量的其它關(guān)聯(lián)模型。例如,一篇博客文章可能會有無限多條評論。正如其它所有的 Eloquent 關(guān)聯(lián)一樣,一對多關(guān)聯(lián)的定義也是在 Eloquent 模型中寫一個(gè)方法:
<?php namespace App; use Illuminate\Database\Eloquent\Model; class Post extends Model{ /** * 獲取博客文章的評論 */ public function comments() { return $this->hasMany('App\Comment'); } }
記住一點(diǎn),Eloquent 將會自動(dòng)確定 Comment
模型的外鍵屬性。按照約定,Eloquent 將會使用所屬模型名稱的 『snake case』形式,再加上 _id
后綴作為外鍵字段。因此,在上面這個(gè)例子中,Eloquent 將假定 Comment
對應(yīng)到 Post
模型上的外鍵就是 post_id
。
一旦關(guān)系被定義好以后,就可以通過訪問 Post
模型的 comments
屬性來獲取評論的集合。記住,由于 Eloquent 提供了『動(dòng)態(tài)屬性』 ,所以我們可以像訪問模型的屬性一樣訪問關(guān)聯(lián)方法:
$comments = App\Post::find(1)->comments;foreach ($comments as $comment) { //}
當(dāng)然,由于所有的關(guān)聯(lián)還可以作為查詢語句構(gòu)造器使用,因此你可以使用鏈?zhǔn)秸{(diào)用的方式,在 comments
方法上添加額外的約束條件:
$comment = App\Post::find(1)->comments()->where('title', 'foo')->first();
正如 hasOne
方法一樣,你也可以在使用 hasMany
方法的時(shí)候,通過傳遞額外參數(shù)來覆蓋默認(rèn)使用的外鍵與本地鍵:
return $this->hasMany('App\Comment', 'foreign_key'); return $this->hasMany('App\Comment', 'foreign_key', 'local_key');
一對多(反向)
現(xiàn)在,我們已經(jīng)能獲得一篇文章的所有評論,接著再定義一個(gè)通過評論獲得所屬文章的關(guān)聯(lián)關(guān)系。這個(gè)關(guān)聯(lián)是 hasMany
關(guān)聯(lián)的反向關(guān)聯(lián),需要在子級模型中使用 belongsTo
方法定義它:
<?php namespace App; use Illuminate\Database\Eloquent\Model; class Comment extends Model{ /** * 獲取此評論所屬文章 */ public function post() { return $this->belongsTo('App\Post'); } }
這個(gè)關(guān)系定義好以后,我們就可以通過訪問 Comment
模型的 post
這個(gè)『動(dòng)態(tài)屬性』來獲取關(guān)聯(lián)的 Post
模型了:
$comment = App\Comment::find(1); echo $comment->post->title;
在上面的例子中,Eloquent 會嘗試用 Comment
模型的 post_id
與 Post
模型的 id
進(jìn)行匹配。默認(rèn)外鍵名是 Eloquent 依據(jù)關(guān)聯(lián)名,并在關(guān)聯(lián)名后加上 _ 再加上主鍵字段名作為后綴確定的。當(dāng)然,如果 Comment
模型的外鍵不是 post_id
,那么可以將自定義鍵名作為第二個(gè)參數(shù)傳遞給 belongsTo
方法:
/** * 獲取此評論所屬文章 */ public function post(){ return $this->belongsTo('App\Post', 'foreign_key'); }
如果父級模型沒有使用 id
作為主鍵,或者是希望用不同的字段來連接子級模型,則可以通過給 belongsTo
方法傳遞第三個(gè)參數(shù)的形式指定父級數(shù)據(jù)表的自定義鍵:
/** * 獲取此評論所屬文章 */ public function post(){ return $this->belongsTo('App\Post', 'foreign_key', 'other_key'); }
多對多
多對多關(guān)聯(lián)比 hasOne
和 hasMany
關(guān)聯(lián)稍微復(fù)雜些。舉個(gè)例子,一個(gè)用戶可以擁有很多種角色,同時(shí)這些角色也被其他用戶共享。例如,許多用戶可能都有 「管理員」 這個(gè)角色。要定義這種關(guān)聯(lián),需要三個(gè)數(shù)據(jù)庫表: users
,roles
和 role_user
。role_user
表的命名是由關(guān)聯(lián)的兩個(gè)模型按照字母順序來的,并且包含了 user_id
和 role_id
字段。
多對多關(guān)聯(lián)通過調(diào)用 belongsToMany
這個(gè)內(nèi)部方法返回的結(jié)果來定義,例如,我們在 User
模型中定義 roles
方法:
<?php namespace App; use Illuminate\Database\Eloquent\Model;class User extends Model{ /** * 用戶擁有的角色 */ public function roles() { return $this->belongsToMany('App\Role'); } }
一旦關(guān)聯(lián)關(guān)系被定義后,你可以通過 roles
動(dòng)態(tài)屬性獲取用戶角色:
$user = App\User::find(1); foreach ($user->roles as $role) { // }
當(dāng)然,像其它所有關(guān)聯(lián)模型一樣,你可以使用 roles
方法,利用鏈?zhǔn)秸{(diào)用對查詢語句添加約束條件:
$roles = App\User::find(1)->roles()->orderBy('name')->get();
正如前面所提到的,為了確定關(guān)聯(lián)連接表的表名,Eloquent 會按照字母順序連接兩個(gè)關(guān)聯(lián)模型的名字。當(dāng)然,你也可以不使用這種約定,傳遞第二個(gè)參數(shù)到 belongsToMany
方法即可:
return $this->belongsToMany('App\Role', 'role_user');
除了自定義連接表的表名,你還可以通過傳遞額外的參數(shù)到 belongsToMany
方法來定義該表中字段的鍵名。第三個(gè)參數(shù)是定義此關(guān)聯(lián)的模型在連接表里的外鍵名,第四個(gè)參數(shù)是另一個(gè)模型在連接表里的外鍵名:
return $this->belongsToMany('App\Role', 'role_user', 'user_id', 'role_id');
定義反向關(guān)聯(lián)
要定義多對多的反向關(guān)聯(lián), 你只需要在關(guān)聯(lián)模型中調(diào)用 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
方法,自定義連接表表名和自定義連接表里的鍵的字段名稱在這里同樣適用。
獲取中間表字段
就如你剛才所了解的一樣,多對多的關(guān)聯(lián)關(guān)系需要一個(gè)中間表來提供支持, Eloquent 提供了一些有用的方法來和這張表進(jìn)行交互。例如,假設(shè)我們的 User
對象關(guān)聯(lián)了多個(gè) Role
對象。在獲得這些關(guān)聯(lián)對象后,可以使用模型的 pivot
屬性訪問中間表的數(shù)據(jù):
$user = App\User::find(1); foreach ($user->roles as $role) { echo $role->pivot->created_at; }
需要注意的是,我們獲取的每個(gè) Role
模型對象,都會被自動(dòng)賦予 pivot
屬性,它代表中間表的一個(gè)模型對象,并且可以像其他的 Eloquent 模型一樣使用。
默認(rèn)情況下,pivot
對象只包含兩個(gè)關(guān)聯(lián)模型的主鍵,如果你的中間表里還有其他額外字段,你必須在定義關(guān)聯(lián)時(shí)明確指出:
return $this->belongsToMany('App\Role')->withPivot('column1', 'column2');
如果你想讓中間表自動(dòng)維護(hù) created_at
和 updated_at
時(shí)間戳,那么在定義關(guān)聯(lián)時(shí)附加上 withTimestamps
方法即可:
return $this->belongsToMany('App\Role')->withTimestamps();
自定義 pivot
屬性名稱
如前所述,來自中間表的屬性可以使用 pivot
屬性訪問。但是,你可以自由定制此屬性的名稱,以便更好的反應(yīng)其在應(yīng)用中的用途。
例如,如果你的應(yīng)用中包含可能訂閱的用戶,則用戶與博客之間可能存在多對多的關(guān)系。如果是這種情況,你可能希望將中間表訪問器命名為 subscription
取代 pivot
。這可以在定義關(guān)系時(shí)使用 as
方法完成:
return $this->belongsToMany('App\Podcast') ->as('subscription') ->withTimestamps();
一旦定義完成,你可以使用自定義名稱訪問中間表數(shù)據(jù):
$users = User::with('podcasts')->get(); foreach ($users->flatMap->podcasts as $podcast) { echo $podcast->subscription->created_at; }
通過中間表過濾關(guān)系
在定義關(guān)系時(shí),你還可以使用 wherePivot
和 wherePivotIn
方法來過濾 belongsToMany
返回的結(jié)果:
return $this->belongsToMany('App\Role')->wherePivot('approved', 1); return $this->belongsToMany('App\Role')->wherePivotIn('priority', [1, 2]);
定義中間表模型
定義自定義中間表模型
如果你想定義一個(gè)自定義模型來表示關(guān)聯(lián)關(guān)系中的中間表,可以在定義關(guān)聯(lián)時(shí)調(diào)用 using
方法。自定義多對多
中間表模型都必須擴(kuò)展自 Illuminate\Database\Eloquent\Relations\Pivot 類,自定義多對多(多態(tài))
中間表模型必須繼承 Illuminate\Database\Eloquent\Relations\MorphPivot 類。例如,
我們在寫 Role 模型的關(guān)聯(lián)時(shí),使用自定義中間表模型 UserRole
<?php namespace App; use Illuminate\Database\Eloquent\Model; class Role extends Model{ /** * 擁有此角色的所有用戶 */ public function users() { return $this->belongsToMany('App\User')->using('App\UserRole'); } }
當(dāng)定義 UserRole
模型時(shí),我們要擴(kuò)展 Pivot
類:
<?php namespace App; use Illuminate\Database\Eloquent\Relations\Pivot; class UserRole extends Pivot{ // }
你可以組合使用 using
和 withPivot
從中間表來檢索列。例如,通過將列名傳遞給 withPivot
方法,就可以從 UserRole
中間表中檢索出 created_by
和 updated_by
兩列數(shù)據(jù)。
<?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 的自定義中繼模型
如果你用一個(gè)自定義的中繼模型定義了多對多的關(guān)系,而且這個(gè)中繼模型擁有一個(gè)自增的主鍵,你應(yīng)當(dāng)確保這個(gè)自定義中繼模型類中定義了一個(gè) incrementing
屬性其值為 true
:
/** * 標(biāo)識 ID 是否自增。 * * @var bool */ public $incrementing = true;
遠(yuǎn)程一對一關(guān)系
遠(yuǎn)程一對一關(guān)聯(lián)通過一個(gè)中間關(guān)聯(lián)模型實(shí)現(xiàn)。
例如,如果每個(gè)供應(yīng)商都有一個(gè)用戶,并且每個(gè)用戶與一個(gè)用戶歷史記錄相關(guān)聯(lián),那么供應(yīng)商可以通過用戶訪問用戶的歷史記錄,讓我們看看定義這種關(guān)系所需的數(shù)據(jù)庫表:
suppliers id - integer users id - integer supplier_id - integer history id - integer user_id - integer
雖然 history
表不包含 supplier_id
,但 hasOneThrough
關(guān)系可以提供對用戶歷史記錄的訪問,以訪問供應(yīng)商模型?,F(xiàn)在我們已經(jīng)檢查了關(guān)系的表結(jié)構(gòu),讓我們在 Supplier
模型上定義相應(yīng)的方法:
<?php namespace App; use Illuminate\Database\Eloquent\Model; class Supplier extends Model{ /** * 用戶的歷史記錄。 */ public function userHistory() { return $this->hasOneThrough('App\History', 'App\User'); } }
傳遞給 hasOneThrough
方法的第一個(gè)參數(shù)是希望訪問的模型名稱,第二個(gè)參數(shù)是中間模型的名稱。
當(dāng)執(zhí)行關(guān)聯(lián)查詢時(shí),通常會使用 Eloquent 約定的外鍵名。如果你想要自定義關(guān)聯(lián)的鍵,可以通過給 hasOneThrough
方法傳遞第三個(gè)和第四個(gè)參數(shù)實(shí)現(xiàn),第三個(gè)參數(shù)表示中間模型的外鍵名,第四個(gè)參數(shù)表示最終模型的外鍵名。第五個(gè)參數(shù)表示本地鍵名,而第六個(gè)參數(shù)表示中間模型的本地鍵名:
class Supplier extends Model{ /** * 用戶的歷史記錄。 */ public function userHistory() { return $this->hasOneThrough( 'App\History', 'App\User', 'supplier_id', // 用戶表外鍵 'user_id', // 歷史記錄表外鍵 'id', // 供應(yīng)商本地鍵 'id' // 用戶本地鍵 ); } }
遠(yuǎn)程一對多關(guān)聯(lián)
遠(yuǎn)程一對多關(guān)聯(lián)提供了方便、簡短的方式通過中間的關(guān)聯(lián)來獲得遠(yuǎn)層的關(guān)聯(lián)。例如,一個(gè) Country
模型可以通過中間的 User
模型獲得多個(gè) Post
模型。在這個(gè)例子中,你可以輕易地收集給定國家的所有博客文章。讓我們來看看定義這種關(guān)聯(lián)所需的數(shù)據(jù)表:
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
關(guān)聯(lián)能讓我們通過 $country->posts
訪問到一個(gè)國家下所有的用戶文章。為了完成這個(gè)查詢,Eloquent 會先檢查中間表 users
的 country_id
字段,找到所有匹配的用戶 ID 后,使用這些 ID,在 posts
表中完成查找。
現(xiàn)在,我們已經(jīng)知道了定義這種關(guān)聯(lián)所需的數(shù)據(jù)表結(jié)構(gòu),接下來,讓我們在 Country
模型中定義它:
<?php namespace App; use Illuminate\Database\Eloquent\Model; class Country extends Model{ /** * 當(dāng)前國家所有文章。 */ public function posts() { return $this->hasManyThrough('App\Post', 'App\User'); } }
hasManyThrough
方法的第一個(gè)參數(shù)是我們最終希望訪問的模型名稱,而第二個(gè)參數(shù)是中間模型的名稱。
當(dāng)執(zhí)行關(guān)聯(lián)查詢時(shí),通常會使用 Eloquent 約定的外鍵名。如果你想要自定義關(guān)聯(lián)的鍵,可以通過給 hasManyThrough
方法傳遞第三個(gè)和第四個(gè)參數(shù)實(shí)現(xiàn),第三個(gè)參數(shù)表示中間模型的外鍵名,第四個(gè)參數(shù)表示最終模型的外鍵名。第五個(gè)參數(shù)表示本地鍵名,而第六個(gè)參數(shù)表示中間模型的本地鍵名:
class Country extends Model{ public function posts() { return $this->hasManyThrough( 'App\Post', 'App\User', 'country_id', // 用戶表外鍵 'user_id', // 文章表外鍵 'id', // 國家表本地鍵 'id' // 用戶表本地鍵 ); } }
多態(tài)關(guān)聯(lián)
多態(tài)關(guān)聯(lián)允許目標(biāo)模型借助單個(gè)關(guān)聯(lián)從屬于多個(gè)模型。
一對一(多態(tài))
表結(jié)構(gòu)
一對一多態(tài)關(guān)聯(lián)與簡單的一對一關(guān)聯(lián)類似;不過,目標(biāo)模型能夠在一個(gè)關(guān)聯(lián)上從屬于多個(gè)模型。例如,博客 Post
和 User
可能共享一個(gè)關(guān)聯(lián)到 Image
模型的關(guān)系。使用一對一多態(tài)關(guān)聯(lián)允許使用一個(gè)唯一圖片列表同時(shí)用于博客文章和用戶賬戶。讓我們先看看表結(jié)構(gòu):
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
時(shí)使用 imageable_type
列來判斷父模型的 「類型」。
模型結(jié)構(gòu)
接下來,再看看建立關(guān)聯(lián)的模型定義:
<?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'); } }
獲取關(guān)聯(lián)
一旦定義了表和模型,就可以通過模型訪問此關(guān)聯(lián)。比如,要獲取文章圖片,可以使用 image
動(dòng)態(tài)屬性:
$post = App\Post::find(1); $image = $post->image;
還可以通過訪問執(zhí)行 morphTo
調(diào)用的方法名來從多態(tài)模型中獲知父模型。在這個(gè)例子中,就是 Image
模型的 imageable
方法。所以,我們可以像動(dòng)態(tài)屬性那樣訪問這個(gè)方法:
$image = App\Image::find(1); $imageable = $image->imageable;
Image
模型的 imageable
關(guān)聯(lián)將返回 Post
或 User
實(shí)例,其結(jié)果取決于圖片屬性哪個(gè)模型。
一對多(多態(tài))
表結(jié)構(gòu)
一對多多態(tài)關(guān)聯(lián)與簡單的一對多關(guān)聯(lián)類似;不過,目標(biāo)模型可以在一個(gè)關(guān)聯(lián)中從屬于多個(gè)模型。假設(shè)應(yīng)用中的用戶可以同時(shí) 「評論」 文章和視頻。使用多態(tài)關(guān)聯(lián),可以用單個(gè) comments
表同時(shí)滿足這些情況。我們還是先來看看用來構(gòu)建這種關(guān)聯(lián)的表結(jié)構(gòu):
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
模型結(jié)構(gòu)
接下來,看看構(gòu)建這種關(guān)聯(lián)的模型定義:
<?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'); } }
獲取關(guān)聯(lián)
一旦定義了數(shù)據(jù)庫表和模型,就可以通過模型訪問關(guān)聯(lián)。例如,可以使用 comments
動(dòng)態(tài)屬性訪問文章的全部評論:
$post = App\Post::find(1);foreach ($post->comments as $comment) { // }
還可以通過訪問執(zhí)行 morphTo
調(diào)用的方法名來從多態(tài)模型獲取其所屬模型。在本例中,就是 Comment
模型的 commentable
方法:
$comment = App\Comment::find(1); $commentable = $comment->commentable;
Comment
模型的 commentable
關(guān)聯(lián)將返回 Post
或 Video
實(shí)例,其結(jié)果取決于評論所屬的模型。
多對多(多態(tài))
表結(jié)構(gòu)
多對多多態(tài)關(guān)聯(lián)比 morphOne
和 morphMany
關(guān)聯(lián)略微復(fù)雜一些。例如,博客 Post
和 Video
模型能夠共享關(guān)聯(lián)到 Tag
模型的多態(tài)關(guān)系。使用多對多多態(tài)關(guān)聯(lián)允許使用一個(gè)唯一標(biāo)簽在博客文章和視頻間共享。以下是多對多多態(tài)關(guān)聯(lián)的表結(jié)構(gòu):
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
模型結(jié)構(gòu)
接下來,在模型上定義關(guān)聯(lián)。Post
和 Video
模型都有調(diào)用 Eloquent 基類上 morphToMany
方法的 tags
方法:
<?php namespace App; use Illuminate\Database\Eloquent\Model; class Post extends Model{ /** * 獲取文章的所有標(biāo)簽。 */ public function tags() { return $this->morphToMany('App\Tag', 'taggable'); } }
定義反向關(guān)聯(lián)關(guān)系
下面,需要在 Tag
模型上為每個(gè)關(guān)聯(lián)模型定義一個(gè)方法。在這個(gè)示例中,我們將會定義 posts
方法和 videos
方法:
<?php namespace App; use Illuminate\Database\Eloquent\Model; class Tag extends Model{ /** * 獲取被打上此標(biāo)簽的所有文章。 */ public function posts() { return $this->morphedByMany('App\Post', 'taggable'); } /** * 獲取被打上此標(biāo)簽的所有視頻。 */ public function videos() { return $this->morphedByMany('App\Video', 'taggable'); } }
獲取關(guān)聯(lián)
一旦定義了數(shù)據(jù)庫表和模型,就可以通過模型訪問關(guān)聯(lián)。例如,可以使用 tags
動(dòng)態(tài)屬性訪問文章的所有標(biāo)簽:
$post = App\Post::find(1);foreach ($post->tags as $tag) { // }
還可以訪問執(zhí)行 morphedByMany
方法調(diào)用的方法名來從多態(tài)模型獲取其所屬模型。在這個(gè)示例中,就是 Tag
模型的 posts
或 videos
方法。可以像動(dòng)態(tài)屬性一樣訪問這些方法:
$tag = App\Tag::find(1);foreach ($tag->videos as $video) { // }
自定義多態(tài)類型
默認(rèn)情況下, Laravel 使用完全限定類名存儲關(guān)聯(lián)模型類型。在上面的一對多示例中, 因?yàn)?Comment
可能從屬于一個(gè) Post
或一個(gè) Video
,默認(rèn)的 commentable_type
就將分別是 App\Post
或 App\Video
。不過,你可能希望數(shù)據(jù)庫與應(yīng)用的內(nèi)部結(jié)構(gòu)解耦。在這種情況下,可以定義一個(gè) 「morph 映射」 來通知 Eloquent 使用自定義名稱代替對應(yīng)的類名:
use Illuminate\Database\Eloquent\Relations\Relation; Relation::morphMap([ 'posts' => 'App\Post', 'videos' => 'App\Video', ]);
可以在 AppServiceProvider
的 boot
函數(shù)中注冊 morphMap
,或者創(chuàng)建一個(gè)單獨(dú)的服務(wù)提供者。
查詢關(guān)聯(lián)
由于 Eloquent 關(guān)聯(lián)的所有類型都通過方法定義,你可以調(diào)用這些方法,而無需真實(shí)執(zhí)行關(guān)聯(lián)查詢。另外,所有 Eloquent 關(guān)聯(lián)類型用作 查詢構(gòu)造器,允許你在數(shù)據(jù)庫上執(zhí)行 SQL 之前,持續(xù)通過鏈?zhǔn)秸{(diào)用添加約束。
例如,假設(shè)一個(gè)博客系統(tǒng)的 User
模型有許多關(guān)聯(lián)的 Post
模型:
<?php namespace App; use Illuminate\Database\Eloquent\Model; class User extends Model{ /** * 獲取該用戶的所有文章。 */ public function posts() { return $this->hasMany('App\Post'); } }
你可以查詢 posts
關(guān)聯(lián),并為其添加額外的約束:
$user = App\User::find(1); $user->posts()->where('active', 1)->get();
你可以在關(guān)聯(lián)上使用任意 查詢構(gòu)造器 方法,請查閱查詢構(gòu)造器文檔,學(xué)習(xí)那些對你有用的方法。
關(guān)聯(lián)方法 Vs. 動(dòng)態(tài)屬性
如果不需要向 Eloquent 關(guān)聯(lián)查詢添加額外的約束,可以像屬性一樣訪問關(guān)聯(lián)。例如,繼續(xù)使用 User
和 Post
示例模型,可以這樣訪問用戶的全部文章:
$user = App\User::find(1); foreach ($user->posts as $post) { // }
動(dòng)態(tài)屬性是「懶加載」的,這意味著它們僅在你真實(shí)訪問關(guān)聯(lián)數(shù)據(jù)時(shí)才被載入。因此,開發(fā)者經(jīng)常使用 預(yù)加載 預(yù)先加載那些他們確知在載入模型后將訪問的關(guān)聯(lián)。對載入模型關(guān)聯(lián)中必定被執(zhí)行的 SQL 查詢而言,預(yù)加載顯著減少了查詢的執(zhí)行次數(shù)。
查詢已存在的關(guān)聯(lián)
在訪問模型記錄時(shí),可能希望基于關(guān)聯(lián)的存在限制查詢結(jié)果。比如想要獲取至少存在一條評論的所有文章,可以通過給 has
和 orHas
方法傳遞關(guān)聯(lián)名稱來實(shí)現(xiàn):
// 獲取至少存在一條評論的所有文章... $posts = App\Post::has('comments')->get();
還可以指定運(yùn)算符和數(shù)量進(jìn)一步自定義查詢:
// 獲取評論超過三條的文章... $posts = App\Post::has('comments', '>=', 3)->get();
還可以用 「點(diǎn)」語法構(gòu)造嵌套的 has
語句。比如,可以獲取擁有至少一條評論和投票的文章:
// 獲取擁有至少一條帶有投票評論的文章... $posts = App\Post::has('comments.votes')->get();
如果需要更多功能,可以使用 whereHas
和 orWhereHas
方法將「where」 條件放到 has
查詢上。這些方法允許你向關(guān)聯(lián)加入自定義約束,比如檢查評論內(nèi)容:
use Illuminate\Database\Eloquent\Builder; // 獲取至少帶有一條評論內(nèi)容包含 foo% 關(guān)鍵詞的文章... $posts = App\Post::whereHas('comments', function ($query) { $query->where('content', 'like', 'foo%');})->get(); // 獲取至少帶有十條評論內(nèi)容包含 foo% 關(guān)鍵詞的文章... $posts = App\Post::whereHas('comments', function ($query) { $query->where('content', 'like', 'foo%'); }, '>=', 10)->get();
查詢不存在的關(guān)聯(lián)
在訪問模型記錄時(shí),可能希望基于關(guān)聯(lián)不存在來限制查詢結(jié)果。假設(shè)想要獲取不存在任何評論的文章,可以通過向 doesntHave
和 orDoesntHave
方法傳遞關(guān)聯(lián)名稱來實(shí)現(xiàn):
$posts = App\Post::doesntHave('comments')->get();
如果需要更多功能,可以使用 whereDoesntHave
和 orWhereDoesntHave
方法將「where」 條件加到 doesntHave
查詢上。這些方法允許你向關(guān)聯(lián)加入自定義限制,比如檢測評論內(nèi)容:
use Illuminate\Database\Eloquent\Builder; $posts = App\Post::whereDoesntHave('comments', function (Builder $query) { $query->where('content', 'like', 'foo%'); })->get();
還可以使用 「點(diǎn)」 語法執(zhí)行嵌套關(guān)聯(lián)查詢。例如,下面的查詢用于獲取帶有沒被禁用的作者發(fā)表評論的文章:
use Illuminate\Database\Eloquent\Builder; $posts = App\Post::whereDoesntHave('comments.author', function (Builder $query) { $query->where('banned', 1); })->get();
關(guān)聯(lián)模型計(jì)數(shù)
如果想要只計(jì)算關(guān)聯(lián)結(jié)果的統(tǒng)計(jì)數(shù)量而不需要真實(shí)加載它們,可以使用 withCount
方法,它將放在結(jié)果模型的 {relation}_count
列。示例如下:
$posts = App\Post::withCount('comments')->get(); foreach ($posts as $post) { echo $post->comments_count; }
可以像給查詢添加限制一樣為多個(gè)關(guān)系添加「計(jì)數(shù)」:
$posts = App\Post::withCount(['votes', 'comments' => function ($query) { $query->where('content', 'like', 'foo%'); }])->get(); echo $posts[0]->votes_count;echo $posts[0]->comments_count;
還可以給關(guān)聯(lián)計(jì)數(shù)結(jié)果起別名,這允許你在同一關(guān)聯(lián)上添加多個(gè)計(jì)數(shù):
$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
方法之后調(diào)用 withCount
:
$query = App\Post::select(['title', 'body'])->withCount('comments'); echo $posts[0]->title; echo $posts[0]->body; echo $posts[0]->comments_count;
預(yù)加載
當(dāng)以屬性方式訪問 Eloquent 關(guān)聯(lián)時(shí),關(guān)聯(lián)數(shù)據(jù)「懶加載」。這著直到第一次訪問屬性時(shí)關(guān)聯(lián)數(shù)據(jù)才會被真實(shí)加載。不過 Eloquent 能在查詢父模型時(shí)「預(yù)先載入」子關(guān)聯(lián)。預(yù)加載可以緩解 N + 1 查詢問題。為了說明 N + 1 查詢問題,考慮 Book
模型關(guān)聯(lián)到 Author
的情形:
<?php namespace App; use Illuminate\Database\Eloquent\Model; class Book extends Model{ /** * 獲取書籍作者。 */ public function author() { return $this->belongsTo('App\Author'); } }
現(xiàn)在,我們來獲取所有的書籍及其作者:
$books = App\Book::all(); foreach ($books as $book) { echo $book->author->name; }
此循環(huán)將執(zhí)行一個(gè)查詢,用于獲取全部書籍,然后為每本書執(zhí)行獲取作者的查詢。如果我們有 25 本書,此循環(huán)將運(yùn)行 26 個(gè)查詢:1 個(gè)用于查詢書籍,25 個(gè)附加查詢用于查詢每本書的作者。
謝天謝地,我們能夠使用預(yù)加載將操作壓縮到只有 2 個(gè)查詢。在查詢時(shí),可以使用 with
方法指定想要預(yù)加載的關(guān)聯(lián):
$books = App\Book::with('author')->get(); foreach ($books as $book) { echo $book->author->name; }
在這個(gè)例子中,僅執(zhí)行了兩個(gè)查詢:
select * from books select * from authors where id in (1, 2, 3, 4, 5, ...)
預(yù)加載多個(gè)關(guān)聯(lián)
有時(shí),你可能需要在單一操作中預(yù)加載幾個(gè)不同的關(guān)聯(lián)。要達(dá)成此目的,只要向 with
方法傳遞多個(gè)關(guān)聯(lián)名稱構(gòu)成的數(shù)組參數(shù):
$books = App\Book::with(['author', 'publisher'])->get();
嵌套預(yù)加載
可以使用 「點(diǎn)」 語法預(yù)加載嵌套關(guān)聯(lián)。比如在一個(gè) Eloquent 語句中預(yù)加載所有書籍作者及其聯(lián)系方式:
$books = App\Book::with('author.contacts')->get();
預(yù)加載指定列
并不是總需要獲取關(guān)系的每一列。在這種情況下,Eloquent 允許你為關(guān)聯(lián)指定想要獲取的列:
$users = App\Book::with('author:id,name')->get();
{note} 在使用這個(gè)特性時(shí),一定要在要獲取的列的列表中包含
id
列。
為預(yù)加載添加約束
有時(shí),可能希望預(yù)加載一個(gè)關(guān)聯(lián),同時(shí)為預(yù)加載查詢添加額外查詢條件,就像下面的例子:
$users = App\User::with(['posts' => function ($query) { $query->where('title', 'like', '%first%'); }])->get();
在這個(gè)例子中, Eloquent 將僅預(yù)加載那些 title
列包含 first
關(guān)鍵詞的文章。也可以調(diào)用其它的 查詢構(gòu)造器 方法進(jìn)一步自定義預(yù)加載操作:
$users = App\User::with(['posts' => function ($query) { $query->orderBy('created_at', 'desc'); }])->get();
{note} 在約束預(yù)加載時(shí),不能使用
limit
和take
查詢構(gòu)造器方法。
預(yù)加載
有可能你還希望在模型加載完成后在進(jìn)行渴求式加載。舉例來說,如果你想要?jiǎng)討B(tài)的加載關(guān)聯(lián)數(shù)據(jù),那么 load
方法對你來說會非常有用:
$books = App\Book::all(); if ($someCondition) { $books->load('author', 'publisher'); }
如果你想要在渴求式加載的查詢語句中進(jìn)行條件約束,你可以通過數(shù)組的形式去加載,鍵為對應(yīng)的關(guān)聯(lián)關(guān)系,值為 Closure
閉包函數(shù),該閉包的參數(shù)為一個(gè) query
實(shí)例:
$books->load(['author' => function ($query) { $query->orderBy('published_date', 'asc'); }]);
當(dāng)關(guān)聯(lián)關(guān)系沒有被加載時(shí),你可以使用 loadMissing
方法:
public function format(Book $book){ $book->loadMissing('author'); return [ 'name' => $book->name, 'author' => $book->author->name ]; }
嵌套延遲加載 & morphTo
如果希望快速加載 morphTo
關(guān)系,以及該關(guān)系可能返回的各種實(shí)體上的嵌套關(guān)系,可以使用 loadMorph
方法。
這個(gè)方法接受 morphTo
關(guān)系的名稱作為它的第一個(gè)參數(shù),第二個(gè)參數(shù)接收模型數(shù)組、關(guān)系數(shù)組。為了幫助說明這個(gè)方法,可以看一下以下模型例子:
<?php use Illuminate\Database\Eloquent\Model; class ActivityFeed extends Model{ /** * Get the parent of the activity feed record. */ public function parentable() { return $this->morphTo(); } }
在這個(gè)例子中,讓我們假設(shè) Event
、Photo
和 Post
模型可以創(chuàng)建 ActivityFeed
模型。此外,讓我們假設(shè) Event
模型屬于 Calendar
模型,Photo
模型與 Tag
模型相關(guān)聯(lián),Post
模型屬于 Author
模型。
使用這些模型定義和關(guān)系,我們可以檢索 ActivityFeed
模型實(shí)例,并立即加載所有 parentable
模型及其各自的嵌套關(guān)系:
$activities = ActivityFeed::with('parentable') ->get() ->loadMorph('parentable', [ Event::class => ['calendar'], Photo::class => ['tags'], Post::class => ['author'], ]);
插入 & 更新關(guān)聯(lián)模型
保存方法
Eloquent 為新模型添加關(guān)聯(lián)提供了便捷的方法。例如,也許你需要添加一個(gè)新的 Comment
到一個(gè) Post
模型中。你不用在 Comment
中手動(dòng)設(shè)置 post_id
屬性,就可以直接使用關(guān)聯(lián)模型的 save
方法將 Comment
直接插入:
$comment = new App\Comment(['message' => 'A new comment.']); $post = App\Post::find(1); $post->comments()->save($comment);
需要注意的是,我們并沒有使用動(dòng)態(tài)屬性的方式訪問 comments
關(guān)聯(lián)。相反,我們調(diào)用 comments
方法來獲得關(guān)聯(lián)實(shí)例。save
方法將自動(dòng)添加適當(dāng)?shù)?post_id
值到 Comment
模型中。
如果你需要保存多個(gè)關(guān)聯(lián)模型,你可以使用 saveMany
方法:
$post = App\Post::find(1); $post->comments()->saveMany([ new App\Comment(['message' => 'A new comment.']), new App\Comment(['message' => 'Another comment.']), ]);
遞歸保存模型和關(guān)聯(lián)數(shù)據(jù)
如果你想 save
你的模型及其所有關(guān)聯(lián)數(shù)據(jù),你可以使用 push
方法:
$post = App\Post::find(1); $post->comments[0]->message = 'Message'; $post->comments[0]->author->name = 'Author Name';$post->push();
新增方法
除了 save
和 saveMany
方法外,你還可以使用 create
方法。它接受一個(gè)屬性數(shù)組,同時(shí)會創(chuàng)建模型并插入到數(shù)據(jù)庫中。 還有, save
方法和 create
方法的不同之處在于, save
方法接受一個(gè)完整的 Eloquent 模型實(shí)例,而 create
則接受普通的 PHP 數(shù)組:
$post = App\Post::find(1); $comment = $post->comments()->create([ 'message' => 'A new comment.', ]);
{tip} 在使用
create
方法前,請務(wù)必確保查看過本文檔的 批量賦值 章節(jié)。
你還可以使用 createMany
方法去創(chuàng)建多個(gè)關(guān)聯(lián)模型:
$post = App\Post::find(1);$post->comments()->createMany([ [ 'message' => 'A new comment.', ], [ 'message' => 'Another new comment.', ], ]);
你還可以使用 findOrNew
、firstOrNew
、firstOrCreate
和 updateOrCreate
方法來 創(chuàng)建和更新關(guān)系模型.
更新 belongsTo
關(guān)聯(lián)
當(dāng)更新 belongsTo
關(guān)聯(lián)時(shí),可以使用 associate
方法。此方法將會在子模型中設(shè)置外鍵:
$account = App\Account::find(10); $user->account()->associate($account);$user->save();
當(dāng)移除 belongsTo
關(guān)聯(lián)時(shí),可以使用 dissociate
方法。此方法會將關(guān)聯(lián)外鍵設(shè)置為 null
:
$user->account()->dissociate();$user->save();
默認(rèn)模型
belongsTo
關(guān)系允許你指定默認(rèn)模型,當(dāng)給定關(guān)系為 null
時(shí),將會返回默認(rèn)模型。 這種模式被稱作 Null 對象模式 ,可以減少你代碼中不必要的檢查。在下面的例子中,如果發(fā)布的帖子沒有找到作者, user
關(guān)系會返回一個(gè)空的 App\User
模型:
/** * 獲取帖子的作者。 */ public function user(){ return $this->belongsTo('App\User')->withDefault(); }
如果需要在默認(rèn)模型里添加屬性, 你可以傳遞數(shù)組或者回調(diào)方法到 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'; }); }
多對多關(guān)聯(lián)
附加 / 分離
Eloquent 也提供了一些額外的輔助方法,使相關(guān)模型的使用更加方便。例如,我們假設(shè)一個(gè)用戶可以擁有多個(gè)角色,并且每個(gè)角色都可以被多個(gè)用戶共享。給某個(gè)用戶附加一個(gè)角色是通過向中間表插入一條記錄實(shí)現(xiàn)的,可以使用 attach
方法完成該操作:
$user = App\User::find(1); $user->roles()->attach($roleId);
在將關(guān)系附加到模型時(shí),還可以傳遞一組要插入到中間表中的附加數(shù)據(jù):
$user->roles()->attach($roleId, ['expires' => $expires]);
當(dāng)然,有時(shí)也需要移除用戶的角色??梢允褂?detach
移除多對多關(guān)聯(lián)記錄。detach
方法將會移除中間表對應(yīng)的記錄;但是這 2 個(gè)模型都將會保留在數(shù)據(jù)庫中:
// 移除用戶的一個(gè)角色... $user->roles()->detach($roleId); // 移除用戶的所有角色... $user->roles()->detach();
為了方便,attach
和 detach
也允許傳遞一個(gè) ID 數(shù)組:
$user = App\User::find(1); $user->roles()->detach([1, 2, 3]); $user->roles()->attach([ 1 => ['expires' => $expires], 2 => ['expires' => $expires] ]);
同步關(guān)聯(lián)
你也可以使用 sync
方法構(gòu)建多對多關(guān)聯(lián)。sync
方法接收一個(gè) ID 數(shù)組以替換中間表的記錄。中間表記錄中,所有未在 ID 數(shù)組中的記錄都將會被移除。所以該操作結(jié)束后,只有給出數(shù)組的 ID 會被保留在中間表中:
$user->roles()->sync([1, 2, 3]);
你也可以通過 ID 傳遞額外的附加數(shù)據(jù)到中間表:
$user->roles()->sync([1 => ['expires' => true], 2, 3]);
如果你不想移除現(xiàn)有的 ID,可以使用 syncWithoutDetaching
方法:
$user->roles()->syncWithoutDetaching([1, 2, 3]);
切換關(guān)聯(lián)
多對多關(guān)聯(lián)也提供了 toggle
方法用于「切換」給定 ID 數(shù)組的附加狀態(tài)。 如果給定的 ID 已被附加在中間表中,那么它將會被移除,同樣,如果如果給定的 ID 已被移除,它將會被附加:
$user->roles()->toggle([1, 2, 3]);
在中間表上保存額外的數(shù)據(jù)
當(dāng)處理多對多關(guān)聯(lián)時(shí),save 方法接收一個(gè)額外的數(shù)據(jù)數(shù)組作為第二個(gè)參數(shù):
App\User::find(1)->roles()->save($role, ['expires' => $expires]);
更新中間表記錄
如果你需要在中間表中更新一條已存在的記錄,可以使用 updateExistingPivot
。此方法接收中間表的外鍵與要更新的數(shù)據(jù)數(shù)組進(jìn)行更新:
$user = App\User::find(1); $user->roles()->updateExistingPivot($roleId, $attributes);
更新父級時(shí)間戳
當(dāng)一個(gè)模型屬 belongsTo
或者 belongsToMany
另一個(gè)模型時(shí), 例如 Comment
屬于 Post
,有時(shí)更新子模型導(dǎo)致更新父模型時(shí)間戳非常有用。例如,當(dāng) Comment
模型被更新時(shí),您要自動(dòng)「觸發(fā)」父級 Post
模型的 updated_at
時(shí)間戳的更新。Eloquent 讓它變得簡單。只要在子模型加一個(gè)包含關(guān)聯(lián)名稱的 touches
屬性即可:
<?php namespace App; use Illuminate\Database\Eloquent\Model; class Comment extends Model{ /** * 要觸發(fā)的所有關(guān)聯(lián)關(guān)系 * * @var array */ protected $touches = ['post']; /** * 評論所屬的文章 */ public function post() { return $this->belongsTo('App\Post'); } }
現(xiàn)在,當(dāng)你更新一個(gè) Comment
時(shí),對應(yīng)父級 Post
模型的 updated_at
字段同時(shí)也會被更新,使其更方便得知何時(shí)讓一個(gè) Post
模型的緩存失效:
$comment = App\Comment::find(1); $comment->text = 'Edit to this comment!'; $comment->save();