Eloquent:关联
简介
数据表之间经常会互相进行关联。例如,一篇博客文章可能会有多条评论,或是一张订单可能对应一个下单客户。Eloquent 让管理和处理这些关联变得很容易,同时也支持多种类型的关联:
定义关联
你可在 Eloquent 模型类内将 Eloquent 关联定义为函数。因为关联像 Eloquent 模型一样也可以作为强大的 查询语句构造器,定义关联为函数提供了强而有力的链式调用及查找功能。例如:
$user->posts()->where('active', 1)->get();
不过,在深入了解使用关联之前,先让我们来学习如何定义每个类型:
一对一
一对一关联是很基本的关联。例如一个 User 模型也许会对应一个 Phone。要定义这种关联,我们必须将 phone 方法放置于 User 模型上。phone 方法应该要返回基类 Eloquent 上的 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 字段会有个对应值。换句话说,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 会取用自身模型的「蛇形命名」后的名称,并在后方加上 _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', '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');
    }
}
如你所见,此定义除了简单的参考 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('approved', [1, 2]);
远层一对多
「远层一对多」提供了方便简短的方法来通过中间的关联获取远层的关联。例如,一个 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');
    }
}
多态关联
数据表结构
多态关联允许一个模型在单个关联中从属一个以上其它模型。比方说你可为你的工作人员和产品保存一些照片。使用多态关联,你可以对这两种情况使用单个 photos 数据表。让我们先来查看下创建这种关联所需的数据表结构:
staff
    id - integer
    name - string
products
    id - integer
    price - integer
photos
    id - integer
    path - string
    imageable_id - integer
    imageable_type - string
有两个要注意的重要字段是 photos 数据表的 imageable_id 和 imageable_type 字段。imageable_id 字段会包含所属的工作人员或产品的 ID 值,而 imageable_type 字段会包含所属的模型类名称。当访问 imageable 关联时,imageable_type 字段会被 ORM 用于判断所属的模型是哪个「类型」。
模型结构
接着,让我们来查看创建这种关联所需的模型定义:
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Photo extends Model
{
    /**
     * 获取所有拥有的 imageable 模型。
     */
    public function imageable()
    {
        return $this->morphTo();
    }
}
class Staff extends Model
{
    /**
     * 获取所有工作人员的照片。
     */
    public function photos()
    {
        return $this->morphMany('App\Photo', 'imageable');
    }
}
class Product extends Model
{
    /**
     * 获取所有产品的照片。
     */
    public function photos()
    {
        return $this->morphMany('App\Photo', 'imageable');
    }
}
获取多态关联
一旦你的数据表及模型被定义,则可以通过模型来访问关联。例如,若要访问工作人员的所有照片,则可以简单的使用 photos 动态属性:
$staff = App\Staff::find(1);
foreach ($staff->photos as $photo) {
    //
}
你也可以从多态模型的多态关联中,通过访问调用 morphTo 的方法名称来获取拥有者,也就是此例子中 Phone 模型的 imageable 方法。所以,我们可以使用动态属性来访问这个方法:
$photo = App\Photo::find(1);
$imageable = $photo->imageable;
Photo 模型的 imageable 关联会返回 Staff 或 Product 实例,这取决于照片所属模型的类型。
自定义多态关联的类型字段
默认情况下,Laravel 会使用「包含命名空间的类名」作为多态表的类型区分,例如,Post 和 Comment 可以被 Like,likable_type 的值会是 App\Post 或 App\Comment。
然而,你也可以选择自定义自己的「多态对照表」:
use Illuminate\Database\Eloquent\Relations\Relation;
Relation::morphMap([
    App\Post::class,
    App\Comment::class,
]);
或者是对应字段:
use Illuminate\Database\Eloquent\Relations\Relation;
Relation::morphMap([
    'posts' => App\Post::class,
    'comments' => App\Comment::class,
]);
译者注:可以使用 class_basename(App\Post::class) 来得到
Post
你可以在 AppServiceProvider 中注册你的「多态对照表」,或是创建一个单独的提供者文件。
多态多对多关联
数据表结构
除了一般的多态关联,你也可以定义「多对多」的多态关联。例如,博客的 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) {
    //
}