iT邦幫忙

第 11 屆 iThome 鐵人賽

DAY 11
0

今天要做的是建立模型之間的關聯。

介紹

資料庫的資料表通常會互相關聯。例如,部落格文章可能有許多評論,或是訂單會與下單的使用者有關聯。Eloquent 使這些關聯變得更容易管理與運用

在這次的專案會使用到以下三種關聯:

  1. 一對一
  2. 一對多
  3. 多對多

一對一

一對一關聯是相當基本的關聯。例如,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 會在 Phone 記錄上的 user_id 欄位中尋找使用者的 id 欄位。如果你想要關聯使用 id 以外的值,你可以在 hasOne 方法傳入選擇你自訂的鍵作為第三個參數:

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

定義反向的關聯

所以我們能從我們的 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_idUser 模型的 id。Eloquent 透過檢查關聯方法的名稱和使用 _id 後綴方法的名稱來決定預設外鍵名稱。然而,如果在 Phone 上的外鍵不是user_id,你可以在 belongsTo 方法上傳入自訂鍵作為第二個參數:

/**
 * 取得擁有手機的使用者。
 */
public function user()
{
    return $this->belongsTo('App\User', 'foreign_key');
}

如果你的上層模型不是使用 id 作為主鍵,或是你希望以不同的欄位 join 下層模型,你可以傳遞第三參數至 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 屬性來存取 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「動態屬性」取得 CommentPost 模型:

$comment = App\Comment::find(1);

echo $comment->post->title;

在範例中,Eloquent 會嘗試匹配 Comment 模型的 post_idPost 模型的 id。Eloquent 判斷的預設外鍵名稱參考於關聯模型的方法,並在方法名稱後面加上 _id。當然,如果 Comment 模型的外鍵不是 post_id,你可以傳遞自訂的鍵名至 belongsTo 方法的第二個參數:

/**
 * 取得擁有該評論的文章。
 */
public function post()
{
    return $this->belongsTo('App\Post', 'foreign_key');
}

如果你的上層模型不是使用 id 作為主鍵,或是你希望以不同的欄位 join 下層模型,你可以傳遞第三參數至 belongsTo 方法指定上層資料表的自訂鍵:

/**
 * 取得擁有該評論的文章。
 */
public function post()
{
    return $this->belongsTo('App\Post', 'foreign_key', 'other_key');
}

多對多

多對多關聯稍微比 hasOnehasMany 關聯還複雜。這種關聯的例子如,一位使用者可能用有很多身份,而一種身份可能很多使用者都有。舉例來說,很多使用者都擁有「管理者」的身份。要定義這種關聯,需要使用三個資料表:usersrolesrole_user。role_user 表命名是以相關聯的兩個模型資料表,依照字母順序命名,並包含了 user_idrole_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 物件只提供模型的鍵。如果 pivot 資料表包含了其他的屬性,可以在定義關聯方法時指定那些欄位:

return $this->belongsToMany('App\Role')->withPivot('column1', 'column2');

如果你想要樞紐表自動維護 created_atupdated_at 時間戳記,在定義關聯方法時加上 withTimestamps 方法:

return $this->belongsToMany('App\Role')->withTimestamps();

自訂 pivot 屬性名稱

如之前所說的,從中介表中的數行可以在模型上使用 pivot 方法來存取。然而,你可以自由的自訂該屬性的名稱來更好的反映在應用程式中的用途。

例如,如果你的應用程式包含可以訂閱 Podcasts 的使用,使用者與 Podcasts 之間可能存在著多對多的關係。如果遇到這種情況,你可能希望你的中介表來存取器重新命名為 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 方法過濾回傳的結果:

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\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
{
    //
}

上一篇
DAY10 --- Eloquent: ORM
下一篇
DAY12 --- Laravel 控制器(controller)
系列文
砍掉重練啦! 森林系男孩之後端工程師潛水App挑戰計畫27
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言