# Eloquent: 关联
- [简介](#introduction)
- [定义关联](#defining-relationships)
- [一对一](#one-to-one)
- [一对多](#one-to-many)
- [一对多 (反向)](#one-to-many-inverse)
- [多对多](#many-to-many)
- [自定义中间表模型](#defining-custom-intermediate-table-models)
- [远程一对一](#has-one-through)
- [远程一对多](#has-many-through)
- [多态关联](#polymorphic-relationships)
- [一对一](#one-to-one-polymorphic-relations)
- [一对多](#one-to-many-polymorphic-relations)
- [多对多](#many-to-many-polymorphic-relations)
- [自定义多态模型](#custom-polymorphic-types)
- [查询关联](#querying-relations)
- [关联方法 Vs. 动态属性](#relationship-methods-vs-dynamic-properties)
- [基于存在的关联查询](#querying-relationship-existence)
- [基于不存在的关联查询](#querying-relationship-absence)
- [多态的关联查询](#querying-polymorphic-relationships)
- [关联数据计数](#counting-related-models)
- [预加载](#eager-loading)
- [为预加载添加约束](#constraining-eager-loads)
- [延迟预加载](#lazy-eager-loading)
- [插入 & 更新关联模型](#inserting-and-updating-related-models)
- [ `save` 方法](#the-save-method)
- [ `create` 方法](#the-create-method)
- [更新 `Belongs To` 关联](#updating-belongs-to-relationships)
- [多对多关联](#updating-many-to-many-relationships)
- [更新父集时间戳](#touching-parent-timestamps)
## 简介
数据库表通常相互关联。例如,一篇博客文章可能有很多评论,或者一个订单对应一个下单用户。 Eloquent 让这些关联的管理和使用变得简单,并支持多种类型的关联:
- [一对一](#one-to-one)
- [一对多](#one-to-many)
- [多对多](#many-to-many)
- [远程一对一](#has-one-through)
- [远程一对多](#has-many-through)
- [一对一 (多态关联)](#one-to-one-polymorphic-relations)
- [一对多 (多态关联)](#one-to-many-polymorphic-relations)
- [多对多 (多态关联)](#many-to-many-polymorphic-relations)
## 定义关联
Eloquent 关联在 Eloquent 模型类中以方法的形式呈现。如同 Eloquent 模型本身,关联也可以作为强大的 [查询语句构造器](/docs/{{version}}/queries) 使用,提供了强大的链式调用和查询功能。例如,我们可以在 `posts` 关联的链式调用中附加一个约束条件:
$user->posts()->where('active', 1)->get();
不过在深入使用关联之前,让我们先学习如何定义每种关联类型。
### 一对一
一对一是最基本的关联关系。例如,一个 `User` 模型可能关联一个 `Phone` 模型。为了定义这个关联,我们要在 `User` 模型中写一个 `phone` 方法。在 `phone` 方法内部调用 `hasOne` 方法并返回其结果:
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` 模型。我们可以使用与 `hasOne` 方法对应的 `belongsTo` 方法来定义反向关联:
belongsTo('App\User');
}
}
在上面的例子中, Eloquent 会尝试匹配 `Phone` 模型上的 `user_id` 至 `User` 模型上的 `id` 。它是通过检查关系方法的名称并使用 `_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 模型中写一个方法:
hasMany('App\Comment');
}
}
记住一点,Eloquent 将会自动确定 `Comment` 模型的外键属性。按照约定,Eloquent 将会使用所属模型名称的 『snake case』形式,再加上 `_id` 后缀作为外键字段。因此,在上面这个例子中,Eloquent 将假定 `Comment` 对应到 `Post` 模型上的外键就是 `post_id`。
一旦关系被定义好以后,就可以通过访问 `Post` 模型的 `comments` 属性来获取评论的集合。记住,由于 Eloquent 提供了『动态属性』 ,所以我们可以像访问模型的属性一样访问关联方法:
$comments = App\Post::find(1)->comments;
foreach ($comments as $comment) {
//
}
当然,由于所有的关联还可以作为查询语句构造器使用,因此你可以使用链式调用的方式,在 `comments` 方法上添加额外的约束条件:
$comment = 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` 方法定义它:
belongsTo('App\Post');
}
}
这个关系定义好以后,我们就可以通过访问 `Comment` 模型的 `post` 这个『动态属性』来获取关联的 `Post` 模型了:
$comment = App\Comment::find(1);
echo $comment->post->title;
在上面的例子中,Eloquent 会尝试用 `Comment` 模型的 `post_id` 与 `Post` 模型的 `id` 进行匹配。默认外键名是 Eloquent 依据关联名,并在关联名后加上 `_ `再加上主键字段名作为后缀确定的。当然,如果 `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` 字段。
多对多关联通过调用 `belongsToMany` 这个内部方法返回的结果来定义,例如,我们在 `User` 模型中定义 `roles` 方法:
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` 方法:
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` 对象只包含两个关联模型的主键,如果你的中间表里还有其他额外字段,你必须在定义关联时明确指出:
return $this->belongsToMany('App\Role')->withPivot('column1', 'column2');
如果你想让中间表自动维护 `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` 方法来过滤 `belongsToMany` 返回的结果:
return $this->belongsToMany('App\Role')->wherePivot('approved', 1);
return $this->belongsToMany('App\Role')->wherePivotIn('priority', [1, 2]);
### 定义中间表模型
如果你想定义一个自定义模型来表示关联关系中的中间表,可以在定义关联时调用 `using` 方法。自定义多对多中间表模型都必须扩展自 `Illuminate\Database\Eloquent\Relations\Pivot` 类,自定义多对多(多态)中间表模型必须继承 `Illuminate\Database\Eloquent\Relations\MorphPivot` 类。例如,我们在写 `Role` 模型的关联时,使用自定义中间表模型 `RoleUser`:
belongsToMany('App\User')->using('App\RoleUser');
}
}
当定义 `RoleUser` 模型时,我们要扩展 `Pivot` 类:
belongsToMany('App\User')
->using('App\RoleUser')
->withPivot([
'created_by',
'updated_by',
]);
}
}
> **注意:** `Pivot` 模型可能不使用 `SoftDeletes` 特性。 如果您需要软删除数据关联记录,请考虑将您的数据关联模型转换为实际的 Eloquent 模型。
#### 带有递增 ID 的自定义中继模型
如果你用一个自定义的中继模型定义了多对多的关系,而且这个中继模型拥有一个自增的主键,你应当确保这个自定义中继模型类中定义了一个 `incrementing` 属性其值为 `true` :
/**
* 标识 ID 是否自增。
*
* @var bool
*/
public $incrementing = true;
### 远程一对一关系
远程一对一关联通过一个中间关联模型实现。
例如,如果每个供应商都有一个用户,并且每个用户与一个用户历史记录相关联,那么供应商可以通过用户访问用户的历史记录,让我们看看定义这种关系所需的数据库表:
users
id - integer
supplier_id - integer
suppliers
id - integer
history
id - integer
user_id - integer
虽然 `history` 表不包含 `supplier_id` ,但 `hasOneThrough` 关系可以提供对用户历史记录的访问,以访问供应商模型。现在我们已经检查了关系的表结构,让我们在 `Supplier` 模型上定义相应的方法:
hasOneThrough('App\History', 'App\User');
}
}
传递给 `hasOneThrough` 方法的第一个参数是希望访问的模型名称,第二个参数是中间模型的名称。
当执行关联查询时,通常会使用 Eloquent 约定的外键名。如果你想要自定义关联的键,可以通过给 `hasOneThrough` 方法传递第三个和第四个参数实现,第三个参数表示中间模型的外键名,第四个参数表示最终模型的外键名。第五个参数表示本地键名,而第六个参数表示中间模型的本地键名:
class Supplier extends Model
{
/**
* 用户的历史记录。
*/
public function userHistory()
{
return $this->hasOneThrough(
'App\History',
'App\User',
'supplier_id', // 用户表外键
'user_id', // 历史记录表外键
'id', // 供应商本地键
'id' // 用户本地键
);
}
}
### 远程一对多关联
远程「一对多」关联提供了方便、简短的方式通过中间的关联来获得远层的关联。例如,一个 `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` 模型中定义它:
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', // 国家表本地键
'id' // 用户表本地键
);
}
}
## 多态关联
多态关联允许目标模型借助单个关联从属于多个模型。
### 一对一 (多态)
#### 表结构
一对一多态关联与简单的一对一关联类似;不过,目标模型能够在一个关联上从属于多个模型。例如,博客 `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` 列包含的则是父模型的类名。Eloquent 在访问 `imageable` 时使用 `imageable_type` 列来判断父模型的 「类型」。
#### 模型结构
接下来,再看看建立关联的模型定义:
morphTo();
}
}
class Post extends Model
{
/**
* 获取文章图片。
*/
public function image()
{
return $this->morphOne('App\Image', 'imageable');
}
}
class User extends Model
{
/**
* 获取用户图片。
*/
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` 实例,其结果取决于图片属性哪个模型。
### 一对多(多态)
#### 表结构
一对多多态关联与简单的一对多关联类似;不过,目标模型可以在一个关联中从属于多个模型。假设应用中的用户可以同时 「评论」 文章和视频。使用多态关联,可以用单个 `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
#### 模型结构
接下来,看看构建这种关联的模型定义:
morphTo();
}
}
class Post extends Model
{
/**
* 获取此文章的所有评论。
*/
public function comments()
{
return $this->morphMany('App\Comment', 'commentable');
}
}
class Video extends Model
{
/**
* 获取此视频的所有评论。
*/
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` 实例,其结果取决于评论所属的模型。
### 多对多(多态)
#### 表结构
多对多多态关联比 `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` 模型都有调用 Eloquent 基类上 `morphToMany` 方法的 `tags` 方法:
morphToMany('App\Tag', 'taggable');
}
}
#### 定义反向关联关系
下面,需要在 `Tag` 模型上为每个关联模型定义一个方法。在这个示例中,我们将会定义 `posts` 方法和 `videos` 方法:
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) {
//
}
### 自定义多态类型
默认情况下, Laravel 使用完全限定类名存储关联模型类型。在上面的一对多示例中, 因为 `Comment` 可能从属于一个 `Post` 或一个 `Video`,默认的 `commentable_type` 就将分别是 `App\Post` 或 `App\Video`。不过,你可能希望数据库与应用的内部结构解耦。在这种情况下,可以定义一个 「morph 映射」 来通知 Eloquent 使用自定义名称代替对应的类名:
use Illuminate\Database\Eloquent\Relations\Relation;
Relation::morphMap([
'posts' => 'App\Post',
'videos' => 'App\Video',
]);
可以在 `AppServiceProvider` 的 `boot` 函数中注册 `morphMap`,或者创建一个单独的服务提供者。
> {note} 在现有应用程序中添加「morph 映射」时,数据库中仍包含完全限定类的每个可变形 `*_type` 列值都需要转换为其「映射」名称。
## 查询关联
由于 Eloquent 关联的所有类型都通过方法定义,你可以调用这些方法,而无需真实执行关联查询。另外,所有 Eloquent 关联类型用作 [查询构造器](/docs/{{version}}/queries),允许你在数据库上执行 SQL 之前,持续通过链式调用添加约束。
例如,假设一个博客系统的 `User` 模型有许多关联的 `Post`模型:
hasMany('App\Post');
}
}
你可以查询 `posts` 关联,并为其添加额外的约束:
$user = App\User::find(1);
$user->posts()->where('active', 1)->get();
你可以在关联上使用任意 [查询构造器](/docs/{{version}}/queries) 方法,请查阅查询构造器文档,学习那些对你有用的方法。
#### 在关联之后链式添加 `orWhere` 条件
如上所示,你可以在查询关联时自由添加其他约束。 但是,在将 `orWhere` 子句链接到关联时要小心,因为 `orWhere` 子句将在逻辑上与关联约束处于同一级别:
$user->posts()
->where('active', 1)
->orWhere('votes', '>=', 100)
->get();
// select * from posts
// where user_id = ? and active = 1 or votes >= 100
在大多数情况下,你可以使用[约束组](/docs/{{version}}/queries#parameter-grouping) 在括号中对条件检查进行逻辑分组:
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) {
//
}
动态属性是「懒加载」的,这意味着它们仅在你真实访问关联数据时才被载入。因此,开发者经常使用 [预加载](#eager-loading) 预先加载那些他们确知在载入模型后将访问的关联。对载入模型关联中必定被执行的 SQL 查询而言,预加载显著减少了查询的执行次数。
### 查询已存在的关联
在访问模型记录时,可能希望基于关联的存在限制查询结果。比如想要获取至少存在一条评论的所有文章,可以通过给 `has` 和 `orHas` 方法传递关联名称来实现:
// 获取至少存在一条评论的所有文章...
$posts = App\Post::has('comments')->get();
还可以指定运算符和数量进一步自定义查询:
// 获取评论超过三条的文章...
$posts = App\Post::has('comments', '>=', 3)->get();
还可以用 「点」语法构造嵌套的 `has` 语句。比如,可以获取拥有至少一条评论和投票的文章:
// 获取拥有至少一条带有投票评论的文章...
$posts = App\Post::has('comments.votes')->get();
如果需要更多功能,可以使用 `whereHas` 和 `orWhereHas` 方法将「where」 条件放到 `has` 查询上。这些方法允许你向关联加入自定义约束,比如检查评论内容:
use Illuminate\Database\Eloquent\Builder;
// 获取至少带有一条评论内容包含 foo% 关键词的文章...
$posts = App\Post::whereHas('comments', function (Builder $query) {
$query->where('content', 'like', 'foo%');
})->get();
// 获取至少带有十条评论内容包含 foo% 关键词的文章...
$posts = App\Post::whereHas('comments', function (Builder $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;
// 查询与帖子或视频相关并且标题包含 foo 的评论...
$comments = App\Comment::whereHasMorph(
'commentable',
['App\Post', 'App\Video'],
function (Builder $query) {
$query->where('title', 'like', 'foo%');
}
)->get();
// 查询与帖子相关的评论,标题不像 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;
}
可以像给查询添加限制一样为多个关系添加「计数」:
use Illuminate\Database\Eloquent\Builder;
$posts = App\Post::withCount(['votes', 'comments' => function (Builder $query) {
$query->where('content', 'like', 'foo%');
}])->get();
echo $posts[0]->votes_count;
echo $posts[0]->comments_count;
还可以给关联计数结果起别名,这允许你在同一关联上添加多个计数:
use Illuminate\Database\Eloquent\Builder;
$posts = App\Post::withCount([
'comments',
'comments as pending_comments_count' => function (Builder $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')->get();
echo $posts[0]->title;
echo $posts[0]->body;
echo $posts[0]->comments_count;
## 预加载
当以属性方式访问 Eloquent 关联时,关联数据「懒加载」。这意味着直到第一次访问属性时关联数据才会被真实加载。不过 Eloquent 能在查询父模型时「预先载入」子关联。预加载可以缓解 N + 1 查询问题。为了说明 N + 1 查询问题,考虑 `Book` 模型关联到 `Author` 的情形:
belongsTo('App\Author');
}
}
现在,我们来获取所有的书籍及其作者:
$books = App\Book::all();
foreach ($books as $book) {
echo $book->author->name;
}
此循环将执行一个查询,用于获取全部书籍,然后为每本书执行获取作者的查询。如果我们有 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` 方法结合使用。 为了帮助说明这种方法,让我们考虑以下模型:
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 允许你为关联指定想要获取的列:
$books = App\Book::with('author:id,name')->get();
> {note} 在使用这个特性时,一定要在要获取的列的列表中包含 `id` 列。
#### 默认预加载
有时可能希望在查询模型时始终加载某些关联。 为此,你可以在模型上定义 `$with` 属性:
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` 关键词的文章。也可以调用其它的 [查询构造器](/docs/{{version}}/queries) 方法进一步自定义预加载操作:
$users = App\User::with(['posts' => function ($query) {
$query->orderBy('created_at', 'desc');
}])->get();
> {note} 在约束预加载时,不能使用 `limit` 和 `take` 查询构造器方法。
### 延迟预加载
有可能你还希望在模型加载完成后在进行渴求式加载。举例来说,如果你想要根据某个条件动态决定是否加载关联数据,那么 `load` 方法对你来说会非常有用:
$books = App\Book::all();
if ($someCondition) {
$books->load('author', 'publisher');
}
如果你想要在渴求式加载的查询语句中进行条件约束,你可以通过数组的形式去加载,键为对应的关联关系,值为 `Closure` 闭包函数,该闭包的参数为一个 `query` 实例:
$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` 关系的名称作为它的第一个参数,第二个参数接收模型数组、关系数组。为了帮助说明这个方法,可以看一下以下模型例子:
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'],
]);
## 插入 & 更新关联模型
### 保存方法
Eloquent 为新模型添加关联提供了便捷的方法。例如,也许你需要添加一个新的 `Comment` 到一个 `Post` 模型中。你不用在 `Comment`中手动设置 `post_id` 属性, 就可以直接使用关联模型的 `save` 方法将 `Comment` 直接插入:
$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` 你的模型及其所有关联数据,你可以使用 `push` 方法:
$post = App\Post::find(1);
$post->comments[0]->message = 'Message';
$post->comments[0]->author->name = 'Author Name';
$post->push();
### 新增方法
除了 `save` 和 `saveMany` 方法外,你还可以使用 `create` 方法。它接受一个属性数组,同时会创建模型并插入到数据库中。 还有, `save` 方法和 `create` 方法的不同之处在于, `save` 方法接受一个完整的 Eloquent 模型实例,而 `create` 则接受普通的 PHP 数组:
$post = App\Post::find(1);
$comment = $post->comments()->create([
'message' => 'A new comment.',
]);
> {tip} 在使用 `create` 方法前,请务必确保查看过本文档的 [批量赋值](/docs/{{version}}/eloquent#mass-assignment) 章节。
你还可以使用 `createMany` 方法去创建多个关联模型:
$post = App\Post::find(1);
$post->comments()->createMany([
[
'message' => 'A new comment.',
],
[
'message' => 'Another new comment.',
],
]);
你还可以使用 `findOrNew`、`firstOrNew`、`firstOrCreate` 和 `updateOrCreate` 方法来 [创建和更新关系模型](/docs/{{version}}/eloquent#other-creation-methods).
### 更新 `belongsTo` 关联
当更新 `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` 时,将会返回默认模型。 这种模式被称作 [Null 对象模式](https://en.wikipedia.org/wiki/Null_Object_pattern) ,可以减少你代码中不必要的检查。在下面的例子中,如果发布的帖子没有找到作者, `user` 关系会返回一个空的 `App\User` 模型:
/**
* 获取帖子的作者。
*/
public function user()
{
return $this->belongsTo('App\User')->withDefault();
}
如果需要在默认模型里添加属性, 你可以传递数组或者回调方法到 `withDefault` 中:
/**
* 获取帖子的作者。
*/
public function user()
{
return $this->belongsTo('App\User')->withDefault([
'name' => 'Guest Author',
]);
}
/**
* 获取帖子的作者。
*/
public function user()
{
return $this->belongsTo('App\User')->withDefault(function ($user, $post) {
$user->name = 'Guest Author';
});
}
### 多对多关联
#### 附加 / 分离
Eloquent 也提供了一些额外的辅助方法,使相关模型的使用更加方便。例如,我们假设一个用户可以拥有多个角色,并且每个角色都可以被多个用户共享。给某个用户附加一个角色是通过向中间表插入一条记录实现的,可以使用 `attach` 方法完成该操作:
$user = App\User::find(1);
$user->roles()->attach($roleId);
在将关系附加到模型时,还可以传递一组要插入到中间表中的附加数据:
$user->roles()->attach($roleId, ['expires' => $expires]);
当然,有时也需要移除用户的角色。可以使用 `detach` 移除多对多关联记录。`detach` 方法将会移除中间表对应的记录;但是这 2 个模型都将会保留在数据库中:
// 移除用户的一个角色...
$user->roles()->detach($roleId);
// 移除用户的所有角色...
$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 已被附加在中间表中,那么它将会被移除,同样,如果如果给定的 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);
## 更新父级时间戳
当一个模型属 `belongsTo` 或者 `belongsToMany` 另一个模型时, 例如 `Comment` 属于 `Post`,有时更新子模型导致更新父模型时间戳非常有用。例如,当 `Comment` 模型被更新时,您要自动「触发」父级 `Post` 模型的 `updated_at` 时间戳的更新。Eloquent 让它变得简单。只要在子模型加一个包含关联名称的 `touches` 属性即可:
belongsTo('App\Post');
}
}
现在,当你更新一个 `Comment` 时,对应父级 `Post` 模型的 `updated_at` 字段同时也会被更新,使其更方便得知何时让一个 `Post` 模型的缓存失效:
$comment = App\Comment::find(1);
$comment->text = 'Edit to this comment!';
$comment->save();