Eloquent 关联关系
目录
简介
数据表经常要与其它表做关联,比如一篇博客文章可能有很多评论,或者一个订单会被关联到下单用户,Eloquent 让组织和处理这些关联关系变得简单,并且支持多种不同类型的关联关系:
One To One
One To Many
Many To Many
Has One Through
Has Many Through
One To One (多态)
One To Many (多态)
Many To Many (多态)
定义关联关系
Eloquent 关联关系以 Eloquent 模型类方法的方式定义。和 Eloquent 模型本身一样,关联关系也是强大的查询构建器,定义关联关系为方法可以提供功能强大的方法链和查询能力。例如,我们可以添加更多约束条件到 posts 关联关系:
$user->posts()->where('active', 1)->get();
不过,在深入使用关联关系之前,让我们先学习如何定义每种关联类型。
注:关联关系名称不能和属性名冲突,否则模型将不知道要解析的是属性名还是关联关系。
One To One
一对一关联是一个非常简单的关联关系,例如,一个 User 模型有一个与之关联的 Phone 模型。要定义这种关联关系,我们需要将 phone
方法置于User 模型中,phone
方法会调用 Illuminate\Database\Eloquent\Concerns\HasRelationships
trait
中的 hasOne
方法并返回其结果:
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class User extends Model
{
/**
* Get the phone record associated with the user.
*/
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
{
/**
* Get the user that owns the phone.
*/
public function user()
{
return $this->belongsTo('App\User');
}
}
在上面的例子中,Eloquent 默认将会尝试通过 Phone 模型的 user_id 去 User 模型查找与之匹配的记录。Eloquent 通过在关联关系方法名后加 _id
后缀来生成默认的外键名。不过,如果 Phone 模型上的外键不是 user_id,也可以将自定义的键名作为第二个参数传递到 belongsTo 方法:
/**
* Get the user that owns the phone.
*/
public function user()
{
return $this->belongsTo('App\User', 'foreign_key');
}
如果父模型不使用 id 作为主键,或者我们希望使用别的数据列来连接子模型,可以将父表自定义键作为第三个参数传递给 belongsTo
方法:
/**
* Get the user that owns the phone.
*/
public function user()
{
return $this->belongsTo('App\User', 'foreign_key', 'other_key');
}
同样,我们通过传递完整的参数来改写上述示例代码:
return $this->belongsTo('App\User', 'user_id', 'id');
One To Many
“一对多”关联是用于定义单个模型拥有多个其它模型的关联关系。例如,一篇博客文章拥有多条评论,和其他关联关系一样,一对多关联通过在 Eloquent 模型中定义方法来定义:
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Post extends Model
{
/**
* Get the comments for the blog post.
*/
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');
One To Many (Inverse)
现在我们可以访问文章的所有评论了,接下来让我们定义一个关联关系允许通过评论访问所属文章。要定义与 hasMany 相对的关联关系,需要在子模型中定义一个关联方法去调用 belongsTo 方法:
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Comment extends Model
{
/**
* Get the post that owns the comment.
*/
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,或者你想自定义的话:
/**
* Get the post that owns the comment.
*/
public function post()
{
return $this->belongsTo('App\Post', 'foreign_key');
}
如果我们的父模型不使用 id 作为主键,或者你希望通过其他数据列来连接子模型,可以将自定义键名作为第三个参数传递给 belongsTo 方法:
/**
* Get the post that owns the comment.
*/
public function post()
{
return $this->belongsTo('App\Post', 'foreign_key', 'other_key');
}
类似的,通过传递完整参数改写上述调用代码如下:
return $this->belongsTo('App\Post', 'post_id', 'id');
Many To Many
多对多关联比 hasOne
和 hasMany
关联关系要稍微复杂一些。这种关联关系的一个例子就是在权限管理中,一个用户可能有多个角色,同时一个角色可能被多个用户共用。例如,很多用户可能都有一个“Admin”角色。
表结构
要定义这样的关联关系,需要三张数据表:users、roles 和 role_user,role_user 表按照关联模型名的字母顺序命名,并且包含 user_id 和 role_id 两个列:
users
id - integer
name - string
roles
id - integer
name - string
role_user
user_id - integer
role_id - integer
模型结构
多对多关联通过编写调用 belongsToMany
方法返回结果的方式来定义,例如,我们在 User 模型上定义 roles
方法:
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class User extends Model
{
/**
* The roles that belong to the user.
*/
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
{
/**
* The users that belong to the role.
*/
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
和 wherePivotNotIn
方法过滤 belongsToMany
返回的结果集:
return $this->belongsToMany('App\Role')->wherePivot('approved', 1);
return $this->belongsToMany('App\Role')->wherePivotIn('priority', [1, 2]);
return $this->belongsToMany('App\Role')->wherePivotNotIn('priority', [1, 2]);
自定义中间表模型
如果我们想要定义自定义的模型来表示关联关系中间表,可以在定义关联关系的时候调用 using 方法,所有用于表示关联关系中间表的自定义模型都必须继承自 Illuminate\Database\Eloquent\Relations\Pivot
类,用于自定义多态的多对多中间模型则继承自 Illuminate\Database\Eloquent\Relations\MorphPivot
类。例如,我们可以定义一个使用 RoleUser 中间模型的 Role:
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Role extends Model
{
/**
* The users that belong to the role.
*/
public function users()
{
return $this->belongsToMany('App\User')->using('App\RoleUser');
}
}
RoleUser 继承自 Pivot 类:
<?php
namespace App;
use Illuminate\Database\Eloquent\Relations\Pivot;
class RoleUser extends Pivot
{
//
}
我们可以将 using 和 withPivot 联合起来以便从中间表获取字段。例如,我们可以通过传递列名到 withPivot
方法来从 RoleUser 中间表获取 created_by 和 updated_by 字段:
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Role extends Model
{
/**
* The users that belong to the role.
*/
public function users()
{
return $this->belongsToMany('App\User')
->using('App\RoleUser')
->withPivot([
'created_by',
'updated_by'
]);
}
}
注意:Pivot 模型不能使用 SoftDeletes trait,如果你需要软删除中间表记录,需要将中间模型转化为真正的 Eloquent 模型。
自定义中间模型和自增ID
如果我们已经定义过一个使用自定义中间模型的多对多关联关系,并且这个中间模型有一个自增主键,需要确保自定义的中间模型类定义了一个被设置为 true 的 incrementing 属性:
/**
* Indicates if the IDs are auto-incrementing.
*
* @var bool
*/
public $incrementing = true;
Has One Through
“Has One Through” 关联通过单一中间关系链接模型,例如,如果每个供应商都有一个用户,同时每个用户都与一个用户历史记录相关联,这样,供应商模型就可以通过用户来访问用户的历史。下面我们来看看定义这个关联关系所需的数据表结构:
users
id - integer
supplier_id - integer
suppliers
id - integer
history
id - integer
user_id - integer
尽管 history 数据表不包含 supplier_id 列,hasOneThrough 关联仍然可以为供应商提供对用户历史的访问。现在,我们已经知道了关联关系对应的数据表结构,接下来我们在 Supplier 模型上定义这个关联:
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Supplier extends Model
{
/**
* Get the user's history.
*/
public function userHistory()
{
return $this->hasOneThrough('App\History', 'App\User');
}
}
传递给 hasOneThrough
方法的第一个参数是我们最终希望访问的模型类名,第二个参数是中间模型的类名。
和前面几种关联关系一样,在执行这个关联查询时也会应用常规的 Eloquent 外键默认约定,如果你想要自定义关联关系使用的外键,可以将它们作为第三个和第四个参数传递到 hasOneThrough
方法。其中,第三个参数是中间模型的外键名称,第四个参数是最终要访问的模型的外键名称,hasOneThrough 方法还有第五个参数,默认是当前模型的主键名称,以及第六个参数,表示中间模型的主键名称:
class Supplier extends Model
{
/**
* Get the user's history.
*/
public function userHistory()
{
return $this->hasOneThrough(
'App\History',
'App\User',
'supplier_id', // Foreign key on users table...
'user_id', // Foreign key on history table...
'id', // Local key on suppliers table...
'id' // Local key on users table...
);
}
}
Has Many Through
“Has Many Through” 关联为通过中间关联访问远层的关联关系提供了一个便捷之道。例如,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
{
/**
* Get all of the posts for the country.
*/
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', // Foreign key on users table...
'user_id', // Foreign key on posts table...
'id', // Local key on countries table...
'id' // Local key on users table...
);
}
}
多态关联
多态关联允许目标模型在单个关联下归属于多种不同的模型。
One To One(多态)
表结构
一对一的多态关联和简单的一对一关联类似,不同之处在于目标模型在单个关联下可以归属于多种不同的模型。例如,Post 和 User 可以共享与 Image 模型的多态关联。使用一对一多态关联,我们可以拥有一个可用于博客文章和用户账户的唯一图片列表。首先,我们来定义表结构:
posts
id - integer
name - string
users
id - integer
name - string
images
id - integer
url - string
imageable_id - integer
imageable_type - string
注意 images 表中的 imageable_id
和 imageable_type
字段,imageable_id 字段存储的是文章或用户的ID值,而 imageable_type 字段存储的是归属父模型的类名。访问 imageable 关联时,Eloquent 使用 imageable_type 字段来判定返回哪种类型的父模型(Post 还是 User)。
模型结构
接下来,我们来看看用于构建这个关联的模型定义:
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Image extends Model
{
/**
* Get the owning imageable model.
*/
public function imageable()
{
return $this->morphTo();
}
}
class Post extends Model
{
/**
* Get the post's image.
*/
public function image()
{
return $this->morphOne('App\Image', 'imageable');
}
}
class User extends Model
{
/**
* Get the user's image.
*/
public function image()
{
return $this->morphOne('App\Image', 'imageable');
}
}
获取关联关系
定义好数据表和模型类之后,就可以通过模型来访问关联关系了。例如,要获取某篇文章的图片,可以使用 image 动态属性:
$post = App\Post::find(1);
$image = $post->image;
还可以从多态模型中通过访问调用 morphTo
的方法名来获取其归属的父模型。在这个例子中,就是 Image 模型的 imageable
方法,因此,我们可以通过动态属性的方式来访问该方法:
$image = App\Image::find(1);
$imageable = $image->imageable;
Image 模型上的 imageable 关联将会返回 Post 或 User 实例,这取决于哪中模型拥有该image。如果我们需要为关系指定自定义type和id列morphTo,请始终确保将关系名称(应与方法名称完全匹配)作为第一个参数传递:
/**
* Get the model that the image belongs to.
*/
public function imageable()
{
return $this->morphTo(__FUNCTION__, 'imageable_type', 'imageable_id');
}
One To Many(多态)
表结构
一对多的多态关联和简单的一对多关联类似,不同之处在于其目标模型可以通过单个关联归属于多种模型。例如,假设应用用户既可以对文章进行评论也可以对视频进行评论,使用多态关联,我们可以在这两种场景下使用单个 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 the owning commentable model.
*/
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 实例,这取决于哪个类型的模型拥有该评论。
Many To Many(多态)
表结构
多对多的多态关联比 morphOne 和 morphMany 关联稍微复杂一些。例如,一个博客的 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
{
/**
* Get all of the tags for the post.
*/
public function tags()
{
return $this->morphToMany('App\Tag', 'taggable');
}
}
定义相对的关联关系
接下来,在 Tag 模型中,应该为每一个关联模型定义一个方法,例如,我们定义一个 posts
方法和 videos
方法:
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Tag extends Model
{
/**
* Get all of the posts that are assigned this tag.
*/
public function posts()
{
return $this->morphedByMany('App\Post', 'taggable');
}
/**
* Get all of the videos that are assigned this tag.
*/
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) {
//
}
自定义多态类型
默认情况下,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,如果需要的话,也可以创建一个独立的服务提供者来实现这一功能。
注:当添加「多态映射」到已存在的应用时,数据库中每个仍然包含完全限定类名的多态 *_type 字段值都需要转化为对应的映射名。
我们可以在运行时使用 getMorphClass 方法获取给定模型的多态别名,相对的,你可以通过 Relation::getMorphedModel
方法获取与该别名关联的完全限定类名:
use Illuminate\Database\Eloquent\Relations\Relation;
$alias = $post->getMorphClass();
$class = Relation::getMorphedModel($alias);
动态关系
我们可以在运行时使用resolveRelationUsing
方法定义Eloquent模型之间的关系。虽然通常不推荐用于正常的应用程序开发,但这在开发 Laravel 包时偶尔会很有用:
use App\Order;
use App\Customer;
Order::resolveRelationUsing('customer', function ($orderModel) {
return $orderModel->belongsTo(Customer::class, 'customer_id');
});
关联查询
由于 Eloquent 所有关联关系都是通过方法定义,我们可以调用这些方法来获取关联关系的实例而不需要再去手动执行关联查询。此外,所有 Eloquent 关联关系类型同时也是查询构建器,允许我们在最终数据库执行 SQL 之前继续添加条件约束到关联查询上。
例如,假定在一个博客系统中一个 User 模型有很多相关的 Post
模型:
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class User extends Model
{
/**
* Get all of the posts for the user.
*/
public function posts()
{
return $this->hasMany('App\Post');
}
}
我们可以像这样查询 posts 关联并添加额外的条件约束到该关联关系上:
$user = App\User::find(1);
$user->posts()->where('active', 1)->get();
我们可以在关联关系上使用任何查询构建器提供任何的方法!所以,掌握查询构建器的使用是掌握所有 Laravel 数据库操作的重要基石。
在关联查询后链接 orWhere
正如上面示例所演示的,我们可以自由添加额外约束到关联查询,不过,在链接 orWhere
子句到关联查询时要小心,因为 orWhere
子句在逻辑上与关联查询约束条件处于同一级别:
$user->posts()
->where('active', 1)
->orWhere('votes', '>=', 100)
->get();
// select * from posts
// where user_id = ? and active = 1 or votes >= 100
在大多数场景中,你可能倾向使用分组约束对括号之间的条件检查进行逻辑分组:
use Illuminate\Database\Eloquent\Builder;
$user->posts()
->where(function (Builder $query) {
return $query->where('active', 1)
->orWhere('votes', '>=', 100);
})
->get();
// select * from posts
// where user_id = ? and (active = 1 or votes >= 100)
关联方法 Vs. 动态属性
如果我们不需要添加额外的条件约束到 Eloquent 关联查询,我们可以简单通过动态属性来访问关联对象,例如,还是拿 User 和 Post 模型作为例子,可以像这样访问用户的所有文章:
$user = App\User::find(1);
foreach ($user->posts as $post) {
//
}
动态属性是“懒惰式加载”,意味着当我们真正访问它们的时候才会加载关联数据。正因为如此,开发者经常使用渴求式加载来预加载他们知道在加载模型时要被访问的关联关系。饥渴式加载有效减少了必须要被执行用以加载模型关联的 SQL 查询。
查询存在的关联关系
访问一个模型的记录的时候,我们可能希望基于关联关系是否存在来限制查询结果的数目。例如,假设你想要获取所有至少有一个评论的博客文章,要实现这个功能,可以传递关联关系的名称到 has
和 orHas
方法:
// Retrieve all posts that have at least one comment...
$posts = App\Post::has('comments')->get();
我们还可以指定操作符和数目来自定义查询:
// Retrieve all posts that have three or more comments...
$posts = App\Post::has('comments', '>=', 3)->get();
还可以使用”.
“来构造嵌套 has
语句,例如,要获取所有至少有一条评论及投票的文章:
// Retrieve posts that have at least one comment with votes...
$posts = App\Post::has('comments.votes')->get();
如果我们需要更强大的功能,可以使用 whereHas
和 orWhereHas
方法将“where” 条件放到 has
查询上,这些方法允许我们添加自定义条件约束到关联关系条件约束,例如检查一条评论的内容:
use Illuminate\Database\Eloquent\Builder;
// Retrieve posts with at least one comment containing words like foo%...
$posts = App\Post::whereHas('comments', function ($query) {
$query->where('content', 'like', 'foo%');
})->get();
// Retrieve posts with at least ten comments containing words like foo%...
$posts = App\Post::whereHas('comments', function ($query) {
$query->where('content', 'like', 'foo%');
}, '>=', 10)->get();
无关联结果查询
访问一个模型的记录时,我们可能需要基于缺失关联关系的模型对查询结果进行限定。例如,假设你想要获取所有没有评论的博客文章,可以传递关联关系名称到 doesntHave 和 orDoesntHave 方法来实现:
$posts = App\Post::doesntHave('comments')->get();
如果我们需要更多功能,可以使用 whereDoesntHave
和 orWhereDoesntHave
方法添加更多“where”条件到 doesntHave
查询,这些方法允许你添加自定义约束条件到关联关系约束,例如检查评论内容:
use Illuminate\Database\Eloquent\Builder;
$posts = App\Post::whereDoesntHave('comments', function (Builder $query) {
$query->where('content', 'like', 'foo%');
})->get();
还可以使用“.
”号查询嵌套的关联关系,例如,下面的查询会从有效作者那里获取所有带评论的文章:
use Illuminate\Database\Eloquent\Builder;
$posts = App\Post::whereDoesntHave('comments.author', function (Builder $query) {
$query->where('banned', 1);
})->get();
多态关联查询
为了查询 MorphTo 关联关系是否存在,我们可以使用 whereHasMorph 方法和对应的关联方法:
use Illuminate\Database\Eloquent\Builder;
// Retrieve comments associated to posts or videos with a title like foo%...
$comments = App\Comment::whereHasMorph(
'commentable',
['App\Post', 'App\Video'],
function (Builder $query) {
$query->where('title', 'like', 'foo%');
}
)->get();
// Retrieve comments associated to posts with a title not like foo%...
$comments = App\Comment::whereDoesntHaveMorph(
'commentable',
'App\Post',
function (Builder $query) {
$query->where('title', 'like', 'foo%');
}
)->get();
我们可以使用 $type
参数基于关联模型添加不同的约束:
use Illuminate\Database\Eloquent\Builder;
$comments = App\Comment::whereHasMorph(
'commentable',
['App\Post', 'App\Video'],
function (Builder $query, $type) {
$query->where('title', 'like', 'foo%');
if ($type === 'App\Post') {
$query->orWhere('content', 'like', 'foo%');
}
}
)->get();
除了传递可能的多态模型数组之外,还可以提供通配符*
让 Laravel 从数据库获取所有可能的多态类型。Laravel 会执行一次额外查询来完成这个操作:
use Illuminate\Database\Eloquent\Builder;
$comments = App\Comment::whereHasMorph('commentable', '*', function (Builder $query) {
$query->where('title', '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;
如果我们将 withCount 和 select 语句组合起来使用,需要在 select 方法之后调用 withCount:
$posts = App\Post::select(['title', 'body'])->withCount('comments');
echo $posts[0]->title;
echo $posts[0]->body;
echo $posts[0]->comments_count;
此外,使用 loadCount
方法,我们可以在父模型获取之后加载关联关系统计:
$book = App\Book::first();
$book->loadCount('genres');
如果我们需要在渴求式加载上设置额外的查询约束,可以传递一个你希望加载的关联关系键数组。该数组值应该是一个接收查询构建器实例作为参数的闭包:
$book->loadCount(['reviews' => function ($query) {
$query->where('rating', 5);
}])
计算多态关系的相关模型
如果我们想预先加载morphTo
关系,以及对该关系可能返回的各种实体的嵌套关系计数,可以将with
方法与morphTo
关系的morphWithCount
方法结合使用。
在这个例子中,我们假设 Photo
和 Post
模型可能创造ActivityFeed模型。另外,假设 Photo
模型与Tag
模型相关联,模型Comment
与Post
模型相关联。
使用这些模型定义和关系,我们可以检索ActivityFeed模型实例并预先加载所有parentable模型及其各自的嵌套关系计数:
use Illuminate\Database\Eloquent\Relations\MorphTo;
$activities = ActivityFeed::query()
->with(['parentable' => function (MorphTo $morphTo) {
$morphTo->morphWithCount([
Photo::class => ['tags'],
Post::class => ['comments'],
]);
}])->get();
此外,如果已经检索到ActivityFeed
模型,我们可以使用loadMorphCount
方法预先加载多态关系的各种实体上的所有嵌套关系计数:
$activities = ActivityFeed::with('parentable')
->get()
->loadMorphCount('parentable', [
Photo::class => ['tags'],
Post::class => ['comments'],
]);
饥渴式加载
当以属性方式访问 Eloquent 关联关系的时候,关联关系数据是「懒惰式加载」的,这意味着关联关系数据直到第一次访问的时候才被加载。不过,Eloquent 还可以在查询父级模型的同时「渴求式加载」关联关系。渴求式加载缓解 N+1 查询问题,要阐明 N+1 查询问题,查看关联到 Author 的 Book 模型:
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Book extends Model
{
/**
* Get the author that wrote the book.
*/
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();
嵌套饥渴式加载 morphTo 关联关系
如果我们想要饥渴式加载 morphTo
关联关系,以及该关联关系可能返回的各种实体中包含的嵌套关联关系,可以结合使用 with
方法和 morphTo
关联关系的 morphWith
方法。我们可以通过下面这个模型来演示该方法的使用:
<?php
use Illuminate\Database\Eloquent\Model;
class ActivityFeed extends Model
{
/**
* Get the parent of the activity feed record.
*/
public function parentable()
{
return $this->morphTo();
}
}
在这个例子中,假设 Event、Photo 和 Post 模型可以创建 ActivityFeed 模型,此外,我们还假设 Event 模型属于 Calendar 模型,Photo 模型与 Tag 模型相关联,Post 模型属于 Author 模型。
使用这些模型定义和关联关系,我们可以获取 ActivityFeed 模型实例并饥渴式加载所有的 parentable 模型以及它们各自嵌套的关联关系:
use Illuminate\Database\Eloquent\Relations\MorphTo;
$activities = ActivityFeed::query()
->with(['parentable' => function (MorphTo $morphTo) {
$morphTo->morphWith([
Event::class => ['calendar'],
Photo::class => ['tags'],
Post::class => ['author'],
]);
}])->get();
饥渴式加载指定字段
并不是每次获取关联关系时都需要所有字段,因此,Eloquent 允许你在关联查询时指定要查询的字段:
$users = App\Book::with('author:id,name')->get();
注:使用这个特性时,id 字段是必须列出的。
默认的饥渴式加载
有时候我们可能想要在获取某个模型时总是加载一些关联关系。要实现这个功能,可以在该模型中定义一个 $with 属性:
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Book extends Model
{
/**
* The relationships that should always be loaded.
*
* @var array
*/
protected $with = ['author'];
/**
* Get the author that wrote the book.
*/
public function author()
{
return $this->belongsTo('App\Author');
}
}
如果想要针对某个查询从 $with 属性中移除某个项,可以使用 without
方法:
$books = App\Book::without('author')->get();
带条件约束的饥渴式加载
有时候我们希望饥渴式加载一个关联关系,但还想为饥渴式加载指定更多的查询条件:
$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();
注:查询构建器方法 limit 和 take 不能在饥渴式加载中使用。
懒惰饥渴式加载
有时候我们需要在父模型已经被获取后饥渴式加载一个关联关系。例如,这在你需要动态决定是否加载关联模型时可能很有用:
$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
];
}
嵌套的懒惰饥渴式加载 & morphTo
如果你想要饥渴式加载一个 morphTo
关联,以及该关联可能返回的各种实体嵌套关联,可以使用 loadMorph
方法。
该方法接收 morphTo
关联名称作为第一个参数,以及一个模型/关联对数组作为第二个参数。我们通过一个示例来说明该方法的使用:
<?php
use Illuminate\Database\Eloquent\Model;
class ActivityFeed extends Model
{
/**
* Get the parent of the activity feed record.
*/
public function parentable()
{
return $this->morphTo();
}
}
在这个例子中,我们假设 Event
、Photo
和 Post
模型可以创建 ActivityFeed
模型。此外,还假设 Event 模型归属于 Calendar
模型,Photo
模型与 Tag
模型相关联,并且 Post
模型归属于 Author
模型。
使用这些模型定义和关联,我们可以获取 ActivityFeed
模型实例并饥渴式加载所有的 parentable 模型及其各自嵌套的关联关系:
$activities = ActivityFeed::with('parentable')
->get()
->loadMorph('parentable', [
Event::class => ['calendar'],
Photo::class => ['tags'],
Post::class => ['author'],
]);
插入 & 更新关联模型
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.']),
]);
save
和saveMany
方法不会添加新的模型到已加载到父模型中的任何内存的关系。如果打算在使用save
或者 saveMany
方法后访问关联关系,可能希望使用refresh
方法重新加载模型及其关系:
$post->comments()->save($comment);
$post->refresh();
// All comments, including the newly saved comment...
$post->comments;
递归保存模型&关联关系
如果我们想要 save 模型及其所有相关的关联关系,可以使用 push 方法:
$post = App\Post::find(1);
$post->comments[0]->message = 'Message';
$post->comments[0]->author->name = 'Author Name';
$post->push();
create 方法
除了 save
和 saveMany
方法外,还可以使用 create
方法,该方法接收属性数组、创建模型、然后插入数据库。save
和 create
的不同之处在于 save
接收整个 Eloquent 模型实例而 create
接收原生 PHP 数组:
$post = App\Post::find(1);
$comment = $post->comments()->create([
'message' => 'A new comment.',
]);
注:使用 create 方法之前确保先浏览属性批量赋值文档。
还可以使用 createMany
方法来创建多个关联模型:
$post = App\Post::find(1);
$post->comments()->createMany([
[
'message' => 'A new comment.',
],
[
'message' => 'Another new comment.',
],
]);
还可以使用 findOrNew
、firstOrNew
、firstOrCreate
和 updateOrCreate
方法来创建和更新关联模型。
Belongs To关联关系
更新 belongsTo
关联的时候,可以使用 associate
方法,该方法会在子模型设置外键:
$account = App\Account::find(10);
$user->account()->associate($account);
$user->save();
移除 belongsTo
关联的时候,可以使用 dissociate
方法。该方法会设置关联关系的外键为 null
:
$user->account()->dissociate();
$user->save();
默认模型
belongsTo
,hasOne
,hasOneThrough
和 morphOne
关联关系允许我们在给定关联关系为 null
的情况下定义一个默认的返回模型,我们将这种模式称之为空对象模式,使用这种模式的好处是不用在代码中编写大量的判断检查逻辑。在下面的例子中,user 关联将会在没有用户与文章关联的情况下返回一个空的 App\User 模型:
/**
* Get the author of the post.
*/
public function user()
{
return $this->belongsTo('App\User')->withDefault();
}
要通过属性填充默认的模型,可以传递数据或闭包到 withDefault
方法:
/**
* Get the author of the post.
*/
public function user()
{
return $this->belongsTo('App\User')->withDefault([
'name' => 'Guest Author',
]);
}
/**
* Get the author of the post.
*/
public function user()
{
return $this->belongsTo('App\User')->withDefault(function ($user, $post) {
$user->name = 'Guest Author';
});
}
Many To Many 关联
附加/分离
处理多对多关联的时候,Eloquent 还提供了一些额外的辅助函数使得处理关联模型变得更加方便。例如,我们假定一个用户可能有多个角色,同时一个角色属于多个用户,要通过在连接模型的中间表中插入记录附加角色到用户上,可以使用 attach
方法:
$user = App\User::find(1);
$user->roles()->attach($roleId);
附加关联关系到模型,还可以以数组形式传递额外被插入数据到中间表:
$user->roles()->attach($roleId, ['expires' => $expires]);
当然,有时候有必要从用户中移除角色,要移除一个多对多关联记录,使用 detach
方法。detach
方法将会从中间表中移除相应的记录;但是,两个模型在数据库中都保持不变:
// Detach a single role from the user...
$user->roles()->detach($roleId);
// Detach all roles from the user...
$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();