# Eloquent:關聯
- [簡介](#introduction)
- [定義關聯](#defining-relationships)
- [一對一](#one-to-one)
- [一對多](#one-to-many)
- [一對多(反向)](#one-to-many-inverse)
- [多對多](#many-to-many)
- [遠層一對多](#has-many-through)
- [多態關聯](#polymorphic-relations)
- [多對多多態關聯](#many-to-many-polymorphic-relations)
- [查詢關聯](#querying-relations)
- [關聯方法 Vs. 動態屬性](#relationship-methods-vs-dynamic-properties)
- [基于存在的關聯查詢](#querying-relationship-existence)
- [基于不存在的關聯查詢](#querying-relationship-absence)
- [關聯數據計數](#counting-related-models)
- [預加載](#eager-loading)
- [為預加載添加約束條件](#constraining-eager-loads)
- [延遲預加載](#lazy-eager-loading)
- [插入 & 更新關聯模型](#inserting-and-updating-related-models)
- [`save` 方法](#the-save-method)
- [`create` 方法](#the-create-method)
- [更新 `belongsTo` 關聯](#updating-belongs-to-relationships)
- [多對多關聯](#updating-many-to-many-relationships)
- [更新父級時間戳](#touching-parent-timestamps)
<a name="introduction"></a>
## 簡介
數據庫表通常相互關聯。 例如,一篇博客文章可能有許多評論,或者一個訂單對應一個下單用戶。Eloquent 讓這些關聯的管理和使用變得簡單,并支持多種類型的關聯:
- [一對一](#one-to-one)
- [一對多](#one-to-many)
- [多對多](#many-to-many)
- [遠層一對多](#has-many-through)
- [多態關聯](#polymorphic-relations)
- [多對多多態關聯](#many-to-many-polymorphic-relations)
<a name="defining-relationships"></a>
## 定義關聯
Eloquent 關聯在 Eloquent 模型類中以方法的形式呈現。如同 Eloquent 模型本身,關聯也可以作為強大的 [查詢語句構造器](/docs/{{version}}/queries) 使用,提供了強大的鏈式調用和查詢功能。例如,我們可以在 `posts` 關聯的鏈式調用中附加一個約束條件:
$user->posts()->where('active', 1)->get();
不過,在深入使用關聯之前,讓我們先學習如何定義每種關聯類型。
<a name="one-to-one"></a>
### 一對一
一對一關聯是最基本的關聯關系。例如,一個 `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 會基于模型名決定外鍵名稱。在當前場景中,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');
}
<a name="default-models"></a>
#### 默認模型
`belongsTo` 關聯允許定義默認模型,這適應于當關聯結果返回的是 `null` 的情況。這種設計模式通常稱為 [空對象模式](https://en.wikipedia.org/wiki/Null_Object_pattern),為您免去了額外的條件判斷代碼。在下面的例子中,`user` 關聯如果沒有找到文章的作者,就會返回一個空的 `App\User` 模型。
/**
* 獲得此文章的作者。
*/
public function user()
{
return $this->belongsTo('App\User')->withDefault();
}
您也可以通過傳遞數組或者使用閉包的形式,填充默認模型的屬性:
/**
* 獲得此文章的作者。
*/
public function user()
{
return $this->belongsTo('App\User')->withDefault([
'name' => '游客',
]);
}
/**
* 獲得此文章的作者。
*/
public function user()
{
return $this->belongsTo('App\User')->withDefault(function ($user) {
$user->name = '游客';
});
}
<a name="one-to-many"></a>
### 一對多
「一對多」關聯用于定義單個模型擁有任意數量的其它關聯模型。例如,一篇博客文章可能會有無限多條評論。就像其它的 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');
<a name="one-to-many-inverse"></a>
### 一對多(反向)
現在,我們已經能獲得一篇文章的所有評論,接著再定義一個通過評論獲得所屬文章的關聯。這個關聯是 `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`方法傳遞第三個參數的形式指定父級數據表的自定義鍵:
/**
* 獲得此評論所屬的文章。
*/
public function post()
{
return $this->belongsTo('App\Post', 'foreign_key', 'other_key');
}
<a name="many-to-many"></a>
### 多對多
多對多關聯比 `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();
#### 通過中間表過濾關聯數據
在定義關聯時,您可以使用 `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
{
//
}
<a name="has-many-through"></a>
### 遠層一對多
「遠層一對多」關聯提供了方便、簡短的方式通過中間的關聯來獲得遠層的關聯。例如,一個 `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' // 用戶表本地鍵...
);
}
}
<a name="polymorphic-relations"></a>
### 多態關聯
#### 數據表結構
多態關聯允許一個模型在單個關聯上屬于多個其他模型。例如,想象一下使用您應用的用戶可以「評論」文章和視頻。使用多態關聯,您可以用一個 `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` 方法注冊「多態映射表」,或者使用一個獨立的服務提供者注冊。
<a name="many-to-many-polymorphic-relations"></a>
### 多對多多態關聯
#### 數據表結構
除了傳統的多態關聯,您也可以定義「多對多」的多態關聯。例如,`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) {
//
}
<a name="querying-relations"></a>
## 查詢關聯
由于所有類型的關聯都通過方法定義,您可以調用這些方法來獲取關聯實例,而不需要實際運行關聯的查詢。此外,所有類型的關聯都可以作為 [查詢語句構造器](/docs/{{version}}/queries) 使用,讓你在向數據庫執行 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();
您可以在關聯上使用任何 [查詢語句構造器](/docs/{{version}}/queries) 的方法,所以,歡迎查閱查詢語句構造器的相關文檔以便了解您可以使用哪些方法。
<a name="relationship-methods-vs-dynamic-properties"></a>
### 關聯方法 Vs. 動態屬性
如果您不需要給 Eloquent 關聯查詢添加額外約束條件,你可以簡單的像訪問屬性一樣訪問關聯。例如,我們剛剛的 `User` 和 `Post` 模型例子中,我們可以這樣訪問所有用戶的文章:
$user = App\User::find(1);
foreach ($user->posts as $post) {
//
}
動態屬性是「懶加載」的,意味著它們的關聯數據只在實際被訪問時才被加載。因此,開發者經常使用 [預加載](#eager-loading) 提前加載他們之后會用到的關聯數據。預加載有效減少了 SQL 語句請求數,避免了重復執行一個模型關聯加載數據、發送 SQL 請求帶來的性能問題。
<a name="querying-relationship-existence"></a>
### 基于存在的關聯查詢
當獲取模型記錄時,您可能希望根據存在的關聯對結果進行限制。例如,您想獲得至少有一條評論的所有博客文章。為了實現這個功能,您可以給 `has` 方法傳遞關聯名稱:
// 獲得所有至少有一條評論的文章...
$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();
<a name="querying-relationship-absence"></a>
### 基于不存在的關聯查詢
當獲取模型記錄時,您可能希望根據不存在的關聯對結果進行限制。例如,您想獲得 **沒有** 任何評論的所有博客文章。為了實現這個功能,您可以給 `doesntHave` 方法傳遞關聯名稱:
$posts = App\Post::doesntHave('comments')->get();
如果您需要更高級的用法,可以使用 `whereDoesntHave` 方法在 `doesntHave` 查詢里設置「where」條件。此方法可以讓你增加自定義條件至關聯約束中,例如對評論內容進行檢查:
$posts = Post::whereDoesntHave('comments', function ($query) {
$query->where('content', 'like', 'foo%');
})->get();
<a name="counting-related-models"></a>
### 關聯數據計數
如果您只想統計結果數而不需要加載實際數據,那么可以使用 `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;
<a name="eager-loading"></a>
## 預加載
當作為屬性訪問 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();
<a name="constraining-eager-loads"></a>
### 為預加載添加約束條件
有時,你可能希望在預加載關聯數據的時候,為查詢指定額外的約束條件。這有個例子:
$users = App\User::with(['posts' => function ($query) {
$query->where('title', 'like', '%first%');
}])->get();
在這個例子中,Eloquent 只會預加載標題里包含 `first` 文本的文章。您也可以調用其它的 [查詢語句構造器](/docs/{{version}}/queries) 進一步自定義預加載約束條件:
$users = App\User::with(['posts' => function ($query) {
$query->orderBy('created_at', 'desc');
}])->get();
<a name="lazy-eager-loading"></a>
### 延遲預加載
有時,您可能需要在獲得父級模型后才去預加載關聯數據。例如,當你需要來動態決定是否加載關聯模型時,這可能很有幫助:
$books = App\Book::all();
if ($someCondition) {
$books->load('author', 'publisher');
}
如果您想設置預加載查詢的額外約束條件,可以通過給 `load` 添加數組鍵的形式達到目的,數組值是接收查詢實例的閉包:
$books->load(['author' => function ($query) {
$query->orderBy('published_date', 'asc');
}]);
<a name="inserting-and-updating-related-models"></a>
## 插入 & 更新關聯模型
<a name="the-save-method"></a>
### `save` 方法
Eloquent 提供了便捷的方法來將新的模型增加至關聯中。例如,也許你需要為一個 `Post` 模型插入一個新的 `Comment`。這是你無須為 `Comment` 手動設置 `posts` 屬性,直接在關聯上使用 `save` 方法插入 `Comment` 即可:
$comment = new App\Comment(['message' => '一條新的評論。']);
$post = App\Post::find(1);
$post->comments()->save($comment);
需要注意的是,我們沒有使用動態屬性形式訪問 `comments` 關聯。相反,我們調用了 `comments` 方法獲得關聯實例。`save` 方法會自動在新的 `Comment` 模型中添加正確的 `post_id`值。
如果您需要保存多個關聯模型,可以使用 `saveMany` 方法:
$post = App\Post::find(1);
$post->comments()->saveMany([
new App\Comment(['message' => '一條新的評論。']),
new App\Comment(['message' => '另一條評論。']),
]);
<a name="the-create-method"></a>
### `create` 方法
除了 `save` 和 `saveMany` 方法,您也可以使用 `create` 方法,它接收一個屬性數組、創建模型并插入數據庫。還有,`save` 和 `create` 的不同之處在于,`save` 接收的是一個完整的 Eloquent 模型實例,而 `create` 接收的是一個純 PHP 數組:
$post = App\Post::find(1);
$comment = $post->comments()->create([
'message' => '一條新的評論。',
]);
> {tip} 在使用 `create` 方法前,請確認您已經瀏覽了本文檔的 [批量賦值](/docs/{{version}}/eloquent#mass-assignment) 章節。
您可以使用 `createMany` 方法保存多個關聯模型:
$post = App\Post::find(1);
$post->comments()->createMany([
[
'message' => '一條新的評論。',
],
[
'message' => '另一條新的評論。',
],
]);
<a name="updating-belongs-to-relationships"></a>
### 更新 `belongsTo` 關聯
當更新 `belongsTo` 關聯時,可以使用 `associate` 方法。此方法會在子模型中設置外鍵:
$account = App\Account::find(10);
$user->account()->associate($account);
$user->save();
當刪除 `belongsTo` 關聯時,可以使用 `dissociate`方法。此方法會設置關聯外鍵為 `null`:
$user->account()->dissociate();
$user->save();
<a name="updating-many-to-many-relationships"></a>
### 多對多關聯
#### 附加 / 移除
Eloquent 也提供了幾個額外的輔助方法,讓操作關聯模型更加便捷。例如:我們假設一個用戶可以擁有多個角色,并且每個角色都可以被多個用戶共享。給某個用戶附加一個角色是通過向中間表插入一條記錄實現的,使用 `attach` 方法:
$user = App\User::find(1);
$user->roles()->attach($roleId);
使用 `attach` 方法時,您也可以通過傳遞一個數組參數向中間表寫入額外數據:
$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 數組,向中間表插入對應關聯數據記錄。所有沒放在數組里的 IDs 都會從中間表里移除。所以,這步操作完成后,只有在數組里的 IDs 會被保留在中間表中。
$user->roles()->sync([1, 2, 3]);
您可以通過 ID 傳遞其他額外的數據到中間表:
$user->roles()->sync([1 => ['expires' => true], 2, 3]);
如果您不想移除現有的 IDs,可以使用 `syncWithoutDetaching` 方法:
$user->roles()->syncWithoutDetaching([1, 2, 3]);
#### 切換關聯
多對多關聯也提供了一個 `toggle` 方法用于「切換」給定 IDs 的附加狀態。如果給定 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);
<a name="touching-parent-timestamps"></a>
## 更新父級時間戳
當一個模型 `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 = '編輯了這條評論!';
$comment->save();
## 譯者署名
| 用戶名 | 頭像 | 職能 | 簽名 |
|---|---|---|---|
| [@baooab](https://laravel-china.org/users/17319) | <img class="avatar-66 rm-style" src="https://dn-phphub.qbox.me/uploads/images/201708/11/17319/KbHzLBdgHs.png?imageView2/1/w/100/h/100"> | 翻譯 | 我在 [這兒](https://github.com/baooab/) |
---
> {note} 歡迎任何形式的轉載,但請務必注明出處,尊重他人勞動共創開源社區。
>
> 轉載請注明:本文檔由 Laravel China 社區 [laravel-china.org](https://laravel-china.org) 組織翻譯,詳見 [翻譯召集帖](https://laravel-china.org/topics/5756/laravel-55-document-translation-call-come-and-join-the-translation)。
>
> 文檔永久地址: https://d.laravel-china.org
- 說明
- 翻譯說明
- 發行說明
- 升級說明
- 貢獻導引
- 入門指南
- 安裝
- 配置信息
- 文件夾結構
- HomeStead
- Valet
- 核心架構
- 請求周期
- 服務容器
- 服務提供者
- 門面(Facades)
- Contracts
- 基礎功能
- 路由
- 中間件
- CSRF 保護
- 控制器
- 請求
- 響應
- 視圖
- 重定向
- Session
- 表單驗證
- 錯誤與日志
- 前端開發
- Blade 模板
- 本地化
- 前端指南
- 編輯資源 Mix
- 安全
- 用戶認證
- API認證
- 用戶授權
- 加密解密
- 哈希
- 重置密碼
- 綜合話題
- Artisan 命令行
- 廣播系統
- 緩存系統
- 集合
- 事件系統
- 文件存儲
- 輔助函數
- 郵件發送
- 消息通知
- 擴展包開發
- 隊列
- 任務調度
- 數據庫
- 快速入門
- 查詢構造器
- 分頁
- 數據庫遷移
- 數據填充
- Redis
- Eloquent ORM
- 快速入門
- 模型關聯
- Eloquent 集合
- 修改器
- API 資源
- 序列化
- 測試
- 快速入門
- HTTP 測試
- 瀏覽器測試 Dusk
- 數據庫測試
- 測試模擬器
- 官方擴展包
- Cashier 交易工具包
- Envoy 部署工具
- Horizon
- Passport OAuth 認證
- Scout 全文搜索
- Socialite 社交化登錄
- 交流說明