1. 程式人生 > >Laravel 5.5 Eloquent ORM - 關聯關係

Laravel 5.5 Eloquent ORM - 關聯關係

簡介

資料表經常要與其它表做關聯,比如一篇部落格文章可能有很多評論,或者一個訂單會被關聯到下單使用者。

Eloquent 讓組織和處理這些關聯關係變得簡單,並且支援多種不同型別的關聯關係:

  • 一對一
  • 一對多
  • 多對多
  • 遠層一對多
  • 多型關聯
  • 多對多的多型關聯

定義關聯關係

關聯關係以 Eloquent 模型類方法的方式定義。

和 Eloquent 模型本身一樣,關聯關係也是強大的查詢構建器,定義關聯關係為方法可以提供功能強大的方法鏈和查詢能力。

例如,我們可以新增更多約束條件到 post 關聯關係:

$user->posts()->where('active', 1)->get();

在深入使用關聯關係之前,讓我們先學習如何定義每種關聯關係。

一對一

一對一關聯是一個最簡單的關聯關係。

例如,一個 User 模型有一個與之關聯的 Phone 模型。

要定義這種關聯關係,我們需要將 phone 方法置於User 模型中,phone 方法呼叫 Illuminate\Database\Eloquent\Concerns\HasRelationships trait 中的 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 將會通過 user 表的 id 值去 phone 表中查詢 user_id 與之匹配的 Phone 記錄。如果你想要關聯關係使用其他值而不是 id,可以傳遞第三個引數到hasOne 來指定自定義的主鍵:

return $this->hasOne('App\Phone', 'foreign_key', 'local_key');

我們通過傳遞完整引數改寫上述示例程式碼:

return $this->hasOne('App\Phone', 'user_id', 'id');  

定義相對的關聯

我們可以從 User 中訪問 Phone 模型,相應地,也可以在 Phone 模型中定義關聯關係從而讓我們可以擁有該手機的 User。

可以使用 belongsTo 方法定義與 hasOne 關聯關係相對的關聯:

<?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 模型查詢與之匹配的記錄。

Eloquent 通過在關聯關係方法名後加 _id 字尾來生成預設的外來鍵名。不過,如果 Phone 模型上的外來鍵不是 user_id,也可以將自定義的鍵名作為第二個引數傳遞到 belongsTo 方法:

return $this->belongsTo('App\User', 'foreign_key');

如果父模型不使用 id 作為主鍵,或者你希望使用別的資料列來連線子模型,可以將父表自定義鍵作為第三個引數傳遞給 belongsTo 方法:

return $this->belongsTo('App\User', 'foreign_key', 'other_key');

同樣,我們通過傳遞完整的引數來改寫上述示例程式碼:

return $this->belongsTo('App\User', 'user_id', 'id');

預設模型

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' => 'Guest Author',
    ]);
}

/**
 * 獲取文章作者
 */
public function user()
{
    return $this->belongsTo('App\User')->withDefault(function ($user) {
        $user->name = 'Guest Author';
    });
}

一對多

“一對多”關聯是用於定義單個模型擁有多個其它模型的關聯關係。

例如,一篇部落格文章擁有多條評論,和其他關聯關係一樣,一對多關聯通過在 Eloquent 模型中定義方法來定義:

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Post extends Model{
    /**
     * 獲取部落格文章的評論
     */
    public function comments()
    {
         return $this->hasMany('App\Comment');
    }
}

記住,Eloquent 會自動判斷 Comment 模型的外來鍵,為方便起見,Eloquent 將擁有者模型名稱加上 _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');

// 在本例中,傳遞完整引數程式碼如下
return $this->hasMany('App\Comment', 'post_id', 'id');

一對多(逆向)

現在我們可以訪問文章的所有評論了,接下來讓我們定義一個關聯關係允許通過評論訪問所屬文章。

要定義與 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 字尾生成預設外來鍵,當然,你也可以通過傳遞自定義外來鍵名作為第二個引數傳遞到 belongsTo 方法,如果你的外來鍵不是 post_id,或者你想自定義的話:

return $this->belongsTo('App\Post', 'foreign_key');

如果你的父模型不使用 id 作為主鍵,或者你希望通過其他資料列來連線子模型,可以將自定義鍵名作為第三個引數傳遞給 belongsTo 方法:

return $this->belongsTo('App\Post', 'foreign_key', 'other_key');

類似的,通過傳遞完整引數改寫上述呼叫程式碼如下:

return $this->belongsTo('App\Post', 'post_id', 'id');

多對多

多對多關聯比 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', 'user_roles');

除了自定義連線表(也叫關係表)的表名,你還可以通過傳遞額外引數到 belongsToMany 方法來自定義該表中欄位的列名。第三個引數是你定義關聯關係模型的外來鍵名稱,第四個引數是你要連線到的模型的外來鍵名稱:

return $this->belongsToMany('App\Role', 'user_roles', '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');
    }
}

正如你所看到的,定義的關聯關係和與其對應的 User 中定義的一模一樣,只是前者引用 App\Role,後者引用 App\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');

如果你想要你的 pivot 表自動包含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 類。

例如,我們可以定義一個使用 UserRole 中間模型的 Role:

<?php

namespace App;

use Illuminate\Database\Relations\Pivot;

class Role extends Pivot
{
    /**
     * The users that belong to the role.
     */
    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', // users表使用的外來鍵...
            'user_id', // posts表使用的外來鍵...
            'id', // countries表主鍵...
            'id' // users表主鍵...
        );
    }
}

多型關聯

表結構

多型關聯允許一個模型在單個關聯下屬於多個不同模型。

例如,假設應用使用者既可以對文章進行評論也可以對視訊進行評論,使用多型關聯,你可以在這兩種場景下使用單個 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 列對應 Post 或 Video 的 ID 值,而 commentable_type 列對應所屬模型的類名。當訪問 commentable 關聯時,ORM 根據 commentable_type 欄位來判斷所屬模型的型別並返回相應模型例項。

模型結構

接下來,讓我們看看構建這種關聯關係需要在模型中定義什麼:

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Comment extends Model
{
    /**
     * Get all of the owning commentable models.
     */
    public function commentable()
    {
        return $this->morphTo();
    }
}

class Post extends Model
{
    /**
     * Get all of the post's comments.
     */
    public function comments()
    {
        return $this->morphMany('App\Comment', 'commentable');
    }
}

class Video extends Model
{
    /**
     * Get all of the video's comments.
     */
    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。不過,有時候你可能需要解除資料庫和應用內部結構之間的耦合,這樣的情況下,可以定義一個 morphMap 關聯來告知 Eloquent 為每個模型使用自定義名稱替代完整類名:

use Illuminate\Database\Eloquent\Relations\Relation;

Relation::morphMap([
    'posts' => 'App\Post',
    'videos' => 'App\Video',
]);

你可以在 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 關聯關係型別同時也是查詢構建器,允許你在最終資料庫執行 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 查詢。

查詢存在的關聯關係

訪問一個模型的記錄的時候,你可能希望基於關聯關係是否存在來限制查詢結果的數目。

例如,假設你想要獲取所有至少有一個評論的部落格文章,要實現這個功能,可以傳遞關聯關係的名稱到 has 方法:

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

你還可以指定操作符和數目來自定義查詢:

// 獲取所有至少有三條評論的文章...
$posts = Post::has('comments', '>=', 3)->get();

還可以使用”.“來構造巢狀 has 語句,例如,你要獲取所有至少有一條評論及投票的文章:

// 獲取所有至少有一條評論獲得投票的文章...
$posts = Post::has('comments.votes')->get();

如果你需要更強大的功能,可以使用 whereHas 和 orWhereHas 方法將 where 條件放到 has 查詢上,這些方法允許你新增自定義條件約束到關聯關係條件約束,例如檢查一條評論的內容:

// 獲取所有至少有一條評論包含foo字樣的文章
$posts = Post::whereHas('comments', function ($query) {
    $query->where('content', 'like', 'foo%');
})->get();

無關聯結果查詢

訪問一個模型的記錄時,你可能需要基於缺失關聯關係的模型對查詢結果進行限定。

例如,假設你想要獲取所有沒有評論的部落格文章,可以傳遞關聯關係名稱到 doesntHave 和 orDoesntHave 方法來實現:

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

如果你需要更多功能,可以使用 whereDoesntHave 和 orWhereDoesntHave 方法新增更多“where”條件到 doesntHave 查詢,這些方法允許你新增自定義約束條件到關聯關係約束,例如檢查評論內容:

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

統計關聯模型

如果你想要在不載入關聯關係的情況下統計關聯結果數目,可以使用 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;

還可以為關聯關係計數結果設定別名,從而允許在一個關聯關係上進行多維度計數:

$posts = App\Post::withCount([
    'comments',
    'comments as pending_comments' => function ($query) {
        $query->where('approved', false);
    }
])->get();

echo $posts[0]->comments_count;

echo $posts[0]->pending_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;
}

先執行 1 次查詢獲取表中的所有書,然後另一個查詢獲取每一本書的作者,因此,如果有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();   

注:使用這個特性時,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');
}

如果你需要設定更多的查詢條件到渴求式載入查詢上,可以傳遞一個包含你想要記載的關聯關係陣列到 load 方法,陣列的值應該是接收查詢例項的閉包:

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

插入 & 更新關聯模型

save 方法

Eloquent 為新增新模型到關聯關係提供了便捷方法。例如,如果你需要插入新的 Comment 到 Post 模型,可以從關聯關係的 save 方法直接插入 Comment 而不是手動設定 Comment 的 post_id 屬性:

$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.']),
]);

create 方法

除了 save 和 saveMany 方法外,還可以使用 create 方法。

save 和 create 的不同之處在於 save 接收整個 Eloquent 模型例項而 create 接收原生 PHP 陣列:

$post = App\Post::find(1);

$comment = $post->comments()->create([
    'message' => 'A new comment.',
]);

還可以使用 createMany 方法來建立多個關聯模型:

$post = App\Post::find(1);

$post->comments()->createMany([
    [
        'message' => 'A new comment.',
    ],
    [
        'message' => 'Another new comment.',
    ],
]);

從屬關聯關係

更新 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 => ['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當前被附加,則取消附加,類似的,如果當前沒有附加,則附加:

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

觸發父級時間戳更新

當一個模型屬於另外一個時,例如 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();