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'
);
}
}