Eloquent: 關聯

sgm4231發表於2020-10-09

簡介
資料庫表通常相互關聯。 例如,一篇部落格文章可能有許多評論,或者一個訂單對應一個下單使用者。Eloquent 讓這些關聯的管理和使用變得簡單,並支援多種型別的關聯:

定義關聯

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 {
/** * 獲取與使用者關聯的電話號碼記錄。 */
public function phone() { return $this->hasOne(‘App\Phone’);
}
}

hasOne 方法的第一個引數是關聯模型的類名。一旦定義了模型關聯,我們就可以使用 Eloquent 動態屬性獲得相關的記錄。動態屬性允許你訪問關係方法就像訪問模型中定義的屬性一樣:

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

Eloquent 會基於模型名決定外來鍵名稱。在這個列子中, 會自動假設 Phone 模型有一個 user_id 的外來鍵。如果你想覆蓋這個約定,可以傳遞第二個引數給 has_one 方法:

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’); }

預設模型

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’ => ‘遊客’, ]); }
/*
* 獲得此文章的作者。 */
public function user() { return $this->belongsTo(‘App\User’)->withDefault(function ($user) { $user->name = ‘遊客’; }); }

一對多

「一對多」關聯用於定義單個模型擁有任意數量的其它關聯模型。例如,一篇部落格文章可能會有無限多條評論。就像其它的 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。

關聯關係定義好後,我們就可以通過訪問 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’);

一對多(反向)

現在,我們已經能獲得一篇文章的所有評論,接著再定義一個通過評論獲得所屬文章的關聯。這個關聯是 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 依據關聯名、並在關聯名後加上 _id 字尾確定的。當然,如果 Comment 模型的外來鍵不是 post_id,那麼可以將自定義鍵名作為第二個引數傳遞給 belongsTo 方法:

/** * 獲得此評論所屬的文章。 */ public function post() { return $this->belongsTo(‘App\Post’, ‘foreign_key’); }

如果父級模型沒有使用 id 作為主鍵,或者是希望用不同的欄位來連線子級模型,則可以通過給 belongsTo 方法傳遞第三個引數的形式指定父級資料表的自定義鍵:

/** * Get the post that owns the comment. */ 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 類。例如,

我們在寫 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 { // }

遠端一對多

「遠端一對多」關聯提供了方便、簡短的方式通過中間的關聯來獲得遠層的關聯。例如,一個 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’ // 使用者表本地鍵… ); } }

多型關聯

資料表結構

多型關聯允許一個模型在單個關聯上屬於多個其他模型。例如,想象一下使用您應用的使用者可以「評論」文章和視訊。使用多型關聯,您可以用一個 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 用來儲存文章或者視訊的 ID 值,而 commentable_type 用來儲存所屬模型的類名。commentable_type 是在我們訪問 commentable 關聯時, 讓 ORM 確定所屬的模型是哪個「型別」。

模型結構

接下來,我們來看看建立這種關聯所需的模型定義:

<?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 例項,這取決於評論所屬的模型型別。

自定義多型關聯的型別欄位

預設,Laravel 會使用完全限定類名作為關聯模型儲存在多型模型上的型別欄位值。比如,在上面的例子中,Comment 屬於 Post 或者 Video,那麼 commentable_type的預設值對應地就是 App\Post 和 App\Video。但是,您可能希望將資料庫與程式內部結構解耦。那樣的話,你可以定義一個「多型對映表」來指示 Eloquent 使用每個模型自定義型別欄位名而不是類名:

use Illuminate\Database\Eloquent\Relations\Relation; Relation::morphMap([ ‘posts’ => ‘App\Post’, ‘videos’ => ‘App\Video’, ]);

您可以在 AppServiceProvider 中的 boot 函式中使用 Relation::morphMap 方法註冊「多型對映表」,或者使用一個獨立的服務提供者註冊。

多對多多型關聯

資料表結構

除了傳統的多型關聯,您也可以定義「多對多」的多型關聯。例如,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) { // }

查詢關聯

由於所有型別的關聯都通過方法定義,您可以呼叫這些方法來獲取關聯例項,而不需要實際執行關聯的查詢。此外,所有型別的關聯都可以作為 查詢語句構造器 使用,讓你在向資料庫執行 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();

您可以在關聯上使用任何 查詢語句構造器 的方法,所以,歡迎查閱查詢語句構造器的相關文件以便了解您可以使用哪些方法。

關聯方法 Vs. 動態屬性

如果您不需要給 Eloquent 關聯查詢新增額外約束條件,你可以簡單的像訪問屬性一樣訪問關聯。例如,我們剛剛的 User 和 Post 模型例子中,我們可以這樣訪問所有使用者的文章:

$user = App\User::find(1); foreach ($user->posts as $post) { // }

動態屬性是「懶載入」的,意味著它們的關聯資料只在實際被訪問時才被載入。因此,開發者經常使用 預載入 提前載入他們之後會用到的關聯資料。預載入有效減少了 SQL 語句請求數,避免了重複執行一個模型關聯載入資料、傳送 SQL 請求帶來的效能問題。

基於存在的關聯查詢

當獲取模型記錄時,您可能希望根據存在的關聯對結果進行限制。例如,您想獲得至少有一條評論的所有部落格文章。為了實現這個功能,您可以給 has 或者是 orHas 方法傳遞關聯名稱:

// 獲得所有至少有一條評論的文章… $posts = App\Post::has(‘comments’)->get();

您也可以指定一個運算子和數目,進一步自定義查詢:

// 獲得所有有三條或三條以上評論的文章… $posts = Post::has(‘comments’, ‘>=’, 3)->get();

也可以使用「點」符號構造巢狀的的 has 語句。例如,您可以獲得所有至少有一條獲贊評論的文章:

// 獲得所有至少有一條獲贊評論的文章… $posts = Post::has(‘comments.votes’)->get();

如果您需要更高階的用法,可以使用 whereHas和 orWhereHas 方法在 has 查詢裡設定「where」條件。此方法可以讓你增加自定義條件至關聯約束中,例如對評論內容進行檢查:

// 獲得所有至少有一條評論內容滿足 foo% 條件的文章 $posts = Post::whereHas(‘comments’, function ($query) { $query->where(‘content’, ‘like’, ‘foo%’); })->get();

基於不存在的關聯查詢

當獲取模型記錄時,您可能希望根據不存在的關聯對結果進行限制。例如,您想獲得 沒有 任何評論的所有部落格文章。為了實現這個功能,您可以給 doesntHave 或者 orDoesntHave 方法傳遞關聯名稱:

$posts = App\Post::doesntHave(‘comments’)->get();

如果您需要更高階的用法,可以使用 whereDoesntHave 或者 orWhereDoesntHave 方法在 doesntHave 查詢裡設定「where」條件。此方法可以讓你增加自定義條件至關聯約束中,例如對評論內容進行檢查:

$posts = Post::whereDoesntHave(‘comments’, function ($query) { $query->where(‘content’, ‘like’, ‘foo%’); })->get();

關聯資料計數

如果您只想統計結果數而不需要載入實際資料,那麼可以使用 withCount 方法,此方法會在您的結果集模型中新增一個 {關聯名}_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 = 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;

預載入

當作為屬性訪問模型關聯時,關聯的資料是「懶載入」。意味著關聯的資料在你第一次訪問該屬性的時候才會載入。不過,當你查詢父模型時,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();

延遲預載入

有時,需要在檢索出來的模型上進行預載入。這對動態決定是否預載入就非常實用:

$books = App\Book::all(); if ($someCondition) { $books->load(‘author’, ‘publisher’); }

如果需要在預載入上新增額外的查詢約束,可以傳入一個陣列,關聯為鍵,接受查詢例項的閉包為值:

$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 ]; }

插入 & 更新關聯模型

儲存方法

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 和 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.’, ], ]);

更新 belongsTo 關聯

當更新 belongsTo 關聯時,可以使用 associate 方法。此方法將會在子模型中設定外來鍵:

$account = App\Account::find(10); $user->account()->associate($account); $user->save();

當移除 belongsTo 關聯時,可以使用 dissociate 方法。此方法會將關聯外來鍵設定為 null:

$user->account()->dissociate(); $user->save();

多對多關聯

附加 / 分離

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();

本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章