* * * * *
[TOC]
## 簡介
數據表之間經常會互相進行關聯。例如,一篇博客文章可能會有多條評論,或是一張訂單可能對應一個下單客戶。Eloquent 讓管理和處理這些關聯變得很容易,同時也支持多種類型的關聯:
* [一對一](https://laravel-china.org/docs/laravel/5.4/eloquent-relationships/1265#one-to-one)
* [一對多](https://laravel-china.org/docs/laravel/5.4/eloquent-relationships/1265#one-to-many)
* [多對多](https://laravel-china.org/docs/laravel/5.4/eloquent-relationships/1265#many-to-many)
* [遠層一對多](https://laravel-china.org/docs/laravel/5.4/eloquent-relationships/1265#has-many-through)
* [多態關聯](https://laravel-china.org/docs/laravel/5.4/eloquent-relationships/1265#polymorphic-relations)
* [多態多對多關聯](https://laravel-china.org/docs/laravel/5.4/eloquent-relationships/1265#many-to-many-polymorphic-relations)
## 定義關聯
你可在 Eloquent 模型類內中,把 Eloquent 關聯定義成方法(methods)。因為,關聯就像 Eloquent 模型一樣,也可以作為強大的?[查詢語句構造器](https://laravel-china.org/docs/laravel/5.4/queries),定義關聯為方法,為其提供了強而有力的鏈式調用及查找功能。例如,我們可以在?`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`?外鍵。如果你想要重寫這個約定,則可以傳入第二個參數到?`hasOne`?方法里。
~~~
return $this->hasOne('App\Phone', 'foreign_key');
~~~
此外,Eloquent 假設外鍵會和上層模型的?`id`?字段(或者自定義的?`$primaryKey`)的值相匹配。換句話說,Eloquent 會尋找用戶的?`id`?字段與?`Phone`?模型的?`user_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`。Eloquent 判斷的默認外鍵名稱參考自關聯模型的方法名稱,并會在方法名稱后面加上?`_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_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');
}
}
~~~
一旦關聯被定義之后,則可以通過?`post`?的「動態屬性」來獲取?`Comment`?模型相對應的?`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');
}
~~~
### 多對多
「多對多」關聯要稍微比?`hasOne`?及?`hasMany`?關聯復雜。如,一個用戶可能擁有多種身份,而一種身份能同時被多個用戶擁有。舉例來說,很多用戶都擁有「管理員」的身份。要定義這種關聯,需要使用三個數據表:`users`、`roles`?和?`role_user`。`role_user`?表命名是以相關聯的兩個模型數據表來依照字母順序命名,并包含了?`user_id`?和?`role_id`?字段。
多對多關聯通過編寫一個在自身 Eloquent 類調用的?`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`?對象只提供模型的鍵。如果你的 pivot 數據表包含了其它的屬性,則可以在定義關聯方法時指定那些字段:
~~~
return $this->belongsToMany('App\Role')->withPivot('column1', 'column2');
~~~
如果你想要中間表自動維護?`created_at`?和?`updated_at`?時間戳,可在定義關聯方法時加上?`withTimestamps`?方法:
~~~
return $this->belongsToMany('App\Role')->withTimestamps();
~~~
#### 使用中間表來過濾關聯數據
你可以使用?`wherePivot`?和?`wherePivotIn`?來增加中間件表過濾條件:
~~~
return $this->belongsToMany('App\Role')->wherePivot('approved', 1);
return $this->belongsToMany('App\Role')->wherePivotIn('priority', [1, 2]);
~~~
#### 定義自定義中間表模型
如果你想定義一個自定義模型來表示你中間表的關聯,則可以在定義關聯時調用?`using`?方法。所有用來表示中間表關聯的自定義模型必須擴展自?`Illuminate\Database\Eloquent\Relations\Pivot`?類:
~~~
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Role extends Model
{
/**
* 屬于該身份的用戶。
*/
public function users()
{
return $this->belongsToMany('App\User')->using('App\UserRole');
}
}
~~~
### 遠層一對多
「遠層一對多」提供了方便簡短的方法來通過中間的關聯獲取遠層的關聯。例如,一個?`Country`?模型可能通過中間的?`Users`?模型關聯到多個?`Posts`?模型。讓我們來看看定義此種關聯的數據表:
~~~
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 后,就會在?`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'
);
}
}
~~~
### 多態關聯
#### 數據表結構
多態關聯允許一個模型在單個關聯中從屬一個以上其它模型。舉個例子,想象一下使用你應用的用戶可以「評論」文章和視頻。使用多態關聯關系,您可以使用一個?`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
{
/**
* 獲取所有擁有的 commentable 模型。
*/
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::class,
'videos' => App\Video::class,
]);
~~~
你需要在你自己的?`AppServiceProvider`?中的?`boot`?函數注冊這個?`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) {
//
}
~~~
## 查找關聯
所有類型的 Eloquent 關聯都是通過方法來定義的,你可以通過調用這些方法來獲得關聯的一個實例,而不需要實際運行關聯的查找。此外,所有類型的 Eloquent 關聯也提供了?[查詢語句構造器](https://laravel-china.org/docs/laravel/5.4/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();
~~~
你可以在關聯中使用?[查詢語句構造器](https://laravel-china.org/docs/laravel/5.4/queries)?中的任意方法,所以,歡迎查閱查詢語句構造器的相關文檔以便了解那些有助于你實現的方法。
### 關聯方法與動態屬性
如果你不需要增加額外的條件至 Eloquent 的關聯查找,則可以簡單的像訪問屬性一樣來訪問關聯。例如我們剛剛的?`User`?及?`Post`?模型示例,我們可以像這樣來訪問所有用戶的文章:
~~~
$user = App\User::find(1);
foreach ($user->posts as $post) {
//
}
~~~
動態屬性是「延遲加載」的,意味著它們只會在被訪問的時候才加載關聯數據。正因為如此,開發者通常需要使用?[預加載](https://laravel-china.org/docs/laravel/5.4/eloquent-relationships/1265#eager-loading)來預先加載關聯數據,關聯數據將會在模型加載后被訪問。預加載能有效減少你的 SQL 查詢語句。
### 查找關聯是否存在
當訪問模型的紀錄時,你可能希望根據關聯的存在來對結果進行限制。比方說你想獲取博客中那些至少擁有一條評論的文章。則可以通過傳遞名稱至關聯的?`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();
~~~
### 關聯數據計數
如果你想對關聯數據進行計數但又不想再發起單獨的 SQL 請求,你可以使用?`withCount`?方法,此方法會在你的結果集中增加一個?`{relation}_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;
~~~
## 預加載
當通過屬性訪問 Eloquent 關聯時,該關聯數據會被「延遲加載」。意味著該關聯數據只有在你使用屬性訪問它時才會被加載。不過,Eloquent 可以在你查找上層模型時「預加載」關聯數據。預加載避免了 N + 1 查找的問題。要說明 N + 1 查找的問題,可試想一個關聯到?`Author`?的?`Book`?模型,如下所示:
~~~
<?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;
}
~~~
對于該操作則只會執行兩條 SQL 語句:
~~~
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();
~~~
### 預加載條件限制
有時你可能想要預加載關聯,并且指定預加載查詢的額外條件。如下所示:
~~~
$users = App\User::with(['posts' => function ($query) {
$query->where('title', 'like', '%first%');
}])->get();
~~~
在這個例子里,Eloquent 只會預加載標題包含?`first`?的文章。當然,你也可以調用其它的?[查詢語句構造器](https://laravel-china.org/docs/laravel/5.4/queries)?來進一步自定義預加載的操作:
~~~
$users = App\User::with(['posts' => function ($query) {
$query->orderBy('created_at', 'desc');
}])->get();
~~~
### 延遲預加載
有時你可能需要在上層模型被獲取后才預加載關聯。當你需要來動態決定是否加載關聯模型時會很有幫助,如下所示:
~~~
$books = App\Book::all();
if ($someCondition) {
$books->load('author', 'publisher');
}
~~~
如果你想設置預加載查詢的額外條件,則可以傳遞一個鍵值為你想要的關聯的數組至?`load`?方法。這個數組的值應是用于接收查詢實例的?`閉包`?實例:
~~~
$books->load(['author' => function ($query) {
$query->orderBy('published_date', 'asc');
}]);
~~~
## 寫入關聯模型
### Save 方法
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 方法會自動在新的?`Comment`?模型中增加正確的?`post_id`?值。
如果你需要保存多個關聯模型,則可以使用?`saveMany`?方法:
~~~
$post = App\Post::find(1);
$post->comments()->saveMany([
new App\Comment(['message' => 'A new comment.']),
new App\Comment(['message' => 'Another comment.']),
]);
~~~
### Create 方法
除了?`save`?與?`saveMany`?方法外,你也可以使用?`create`?方法,該方法允許傳入屬性的數組來建立模型并寫入數據庫。`save`?與?`create`?的不同之處在于,`save`?允許傳入一個完整的 Eloquent 模型實例,但?`create`?只允許傳入原始的 PHP 數組:
~~~
$post = App\Post::find(1);
$comment = $post->comments()->create([
'message' => 'A new comment.',
]);
~~~
在使用 create 方法前,請確認你已瀏覽了文檔的?[批量賦值](https://laravel-china.org/docs/laravel/5.4/eloquent#mass-assignment)?章節。
### 更新「從屬」關聯
當更新一個?`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`?方法會從中間表中移除正確的紀錄;當然,這兩個模型依然會存在于數據庫中:
~~~
// 移除用戶身上某一身份...
$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, 3]);
~~~
#### 更新關聯
你也可以使用?`sync`?方法去創建一個多對多的關聯。?`sync`?方法可以用數組形式的 IDs 插入中間的數據表。任何一個不存在于給定數組的 IDs 將會在中間表內被刪除。所以,操作完成之后,只有那些在給定數組內的 IDs 會被保留在中間表中。
~~~
$user->roles()->sync([1, 2, 3]);
~~~
你也可以通過 IDs 傳遞其他的附加中間表值:
~~~
$user->roles()->sync([1 => ['expires' => true], 2, 3]);
~~~
如果你不想分離現有的 IDs ,你可以?`syncWithoutDetaching`?方法:
~~~
$user->roles()->syncWithoutDetaching([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
- HTTP層
- 路由
- 中間件
- CSRF 保護
- 控制器
- 請求
- 響應
- 視圖
- Session
- 表單驗證
- 前端
- Blade 模板
- 本地化
- 前端指南
- 編輯資源 Mix
- 安全
- 用戶認證
- Passport OAuth 認證
- 用戶授權
- 加密解密
- 哈希
- 重置密碼
- 綜合話題
- Artisan 命令行
- 廣播系統
- 緩存系統
- 集合
- 錯誤與日志
- 事件系統
- 文件存儲
- 輔助函數
- 郵件發送
- 消息通知
- 擴展包開發
- 隊列
- 任務調度
- 數據庫
- 快速入門
- 查詢構造器
- 分頁
- 數據庫遷移
- 數據填充
- Redis
- Eloquent ORM
- 快速入門
- 模型關聯
- Eloquent 集合
- 修改器
- 序列化
- 測試
- 快速入門
- HTTP 測試
- 瀏覽器測試 Dusk
- 數據庫測試
- 測試模擬器
- 官方擴展包
- Cashier 交易工具包
- Envoy 部署工具
- Scout 全文搜索
- Socialite 社會化登錄