* * * * *
[TOC]
## 簡介
數據庫表通常相互關聯。 例如,一篇博客文章可能有許多評論,或者一個訂單對應一個下單用戶。Eloquent 讓這些關聯的管理和使用變得簡單,并支持多種類型的關聯:
* [一對一](http://www.hmoore.net/tonyyu/laravel_5_6/786273#_23)
* [一對多](http://www.hmoore.net/tonyyu/laravel_5_6/786273#_149)
* [多對多](http://www.hmoore.net/tonyyu/laravel_5_6/786273#_253)
* [遠程一對多](http://www.hmoore.net/tonyyu/laravel_5_6/786273#_428)
* [多態關聯](http://www.hmoore.net/tonyyu/laravel_5_6/786273#_492)
* [多對多多態關聯](http://www.hmoore.net/tonyyu/laravel_5_6/786273#_600)
## 定義關聯
Eloquent 關聯在 Eloquent 模型類中以方法的形式呈現。如同 Eloquent 模型本身,關聯也可以作為強大的?[查詢語句構造器](http://www.hmoore.net/tonyyu/laravel_5_6/786261)?使用,提供了強大的鏈式調用和查詢功能。例如,我們可以在?`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`?的情況。這種設計模式通常稱為?[空對象模式](https://en.wikipedia.org/wiki/Null_Object_pattern),為您免去了額外的條件判斷代碼。在下面的例子中,`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) {
//
}
~~~
## 查詢關聯
由于所有類型的關聯都通過方法定義,您可以調用這些方法來獲取關聯實例,而不需要實際運行關聯的查詢。此外,所有類型的關聯都可以作為?[查詢語句構造器](http://www.hmoore.net/tonyyu/laravel_5_6/786261)?使用,讓你在向數據庫執行 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();
~~~
您可以在關聯上使用任何?[查詢語句構造器](http://www.hmoore.net/tonyyu/laravel_5_6/786261)?的方法,所以,歡迎查閱查詢語句構造器的相關文檔以便了解您可以使用哪些方法。
### 關聯方法 Vs. 動態屬性
如果您不需要給 Eloquent 關聯查詢添加額外約束條件,你可以簡單的像訪問屬性一樣訪問關聯。例如,我們剛剛的?`User`?和?`Post`?模型例子中,我們可以這樣訪問一個用戶所有的文章:
~~~
$user = App\User::find(1);
foreach ($user->posts as $post) {
//
}
~~~
動態屬性是「懶加載」的,意味著它們的關聯數據只在實際被訪問時才被加載。因此,開發者經常使用?[預加載](http://www.hmoore.net/tonyyu/laravel_5_6/786273#_836)?提前加載他們之后會用到的關聯數據。預加載有效減少了 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`?的帖子。當然,可以調用?[查詢構造器](http://www.hmoore.net/tonyyu/laravel_5_6/786261)?的其他方法,進一步自定義預加載操作:
~~~
$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`?方法前,請務必確保查看過本文檔的?[批量賦值](http://www.hmoore.net/tonyyu/laravel_5_6/786272#_328)?章節。
你還可以使用?`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();
~~~
- 前言
- 翻譯說明
- 發行說明
- 升級指南
- 貢獻導引
- 入門指南
- 安裝
- 配置信息
- 文件夾結構
- Homestead
- Valet
- 部署
- 核心架構
- 請求周期
- 服務容器
- 服務提供者
- Facades
- Contracts
- 基礎功能
- 路由
- 中間件
- CSRF 保護
- 控制器
- 請求
- 響應
- 視圖
- URL
- Session
- 表單驗證
- 錯誤
- 日志
- 前端開發
- Blade 模板
- 本地化
- 前端指南
- 編輯資源 Mix
- 安全相關
- 用戶認證
- Passport OAuth 認證
- 用戶授權
- 加密解密
- 哈希
- 重置密碼
- 綜合話題
- Artisan 命令行
- 廣播系統
- 緩存系統
- 集合
- 事件系統
- 文件存儲
- 輔助函數
- 郵件發送
- 消息通知
- 擴展包開發
- 隊列
- 任務調度
- 數據庫
- 快速入門
- 查詢構造器
- 分頁
- 數據庫遷移
- 數據填充
- Redis
- Eloquent ORM
- 快速入門
- 模型關聯
- Eloquent 集合
- 修改器
- API 資源
- 序列化
- 測試相關
- 快速入門
- HTTP 測試
- 瀏覽器測試 Dusk
- 數據庫測試
- 測試模擬器
- 官方擴展包
- Cashier 交易工具包
- Envoy 部署工具
- Horizon
- Scout 全文搜索
- Socialite 社會化登錄