Eloquent: 关联
简介
数据表之间经常会互相进行关联。例如,一篇博客文章可能会有多条评论,或是一张订单可能对应一个下单客户。Eloquent 让管理和处理这些关联变得很容易,同时也支持多种类型的关联:
定义关联
你可在 Eloquent 模型类内中,把 Eloquent 关联定义成方法(methods)。因为,关联就像 Eloquent 模型一样,也可以作为强大的 查询语句构造器,定义关联为方法,为其提供了强而有力的链式调用及查找功能。例如,我们可以在 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'
        );
    }
}