1. 简介
数据表之间通常有一定的关联。比如,一篇博客文章可能有多条评论,或者一个订单对应一个下单用户。Eloquent 使得我们更容易管理和使用这些关联,下面是支持的几种不同的关联类型:
- 一对一
- 一对多
- 多对多
- 远程一对多
- 多态一对多
- 多态多对多
2. 定义关联
在 Eloquent 模型类中用方法来定义 Eloquent 关联。因此,和 Eloquent 模型自身一样,关联也可以作为强大的查询构造器使用,这也就提供了强大的链式调用和查询功能。例如,我们可以在 posts
关联的链式调用中加上约束条件:
$user->posts()->where('active', 1)->get();
但是,在深入使用关联之前,我们先来看看如何定义每种关联类型。
2.1 一对一
一对一是最基本的关系。例如,一个 User
模型可能关联一个 Phone
模型。定义该关联,我们要在 User
模型中添加一个 phone
方法,在里面调用 hasOne
方法并返回其结果:
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
。如果要使用其它外键名,可以通过传递第二个参数指定:
return $this->hasOne('App\Phone', 'foreign_key');
此外,Eloquent 默认会在 Phone
记录的 user_id
列中查找和用户表的 id
(或自定义 $primaryKey
)相匹配的值。如果不使用 id
,可以通过传递第三个参数指定其它键名:
return $this->hasOne('App\Phone', 'foreign_key', 'local_key');
一对一反向关联
现在,我们已经能从 User
访问 Phone
模型了。接下来,我们在 Phone
上也定义一个关联,来访问拥有该电话的 User
模型。这里使用与 hasOne
对应的 belongsTo
方法来定义相对的关联:
namespace App;
use Illuminate\Database\Eloquent\Model;
class Phone extends Model
{
/**
* 获得拥有此电话的用户
*/
public function user()
{
return $this->belongsTo('App\User');
}
}
上面的例子中,Eloquent 会在 User
记录的 id
列中查找和 user_id
相匹配的值。如果 Phone
模型的外键不是 user_id
,可以通过第二个参数指定其它键名:
/**
* 获得拥有此电话的用户
*/
public function user()
{
return $this->belongsTo('App\User', 'foreign_key');
}
如果 User
模型没有使用 id
作为主键,或者希望使用其它字段,也可以通过传递第三个参数指定键名:
/**
* 获得拥有此电话的用户
*/
public function user()
{
return $this->belongsTo('App\User', 'foreign_key', 'other_key');
}
2.2 一对多
一对多用来定义一个模型拥有多个的其它关联模型。例如,一篇博客文章可能会有多条评论。和其它关联一样,定义一对多关联也是在 Eloquent
模型中添加一个方法:
namespace App;
use Illuminate\Database\Eloquent\Model;
class Post extends Model
{
/**
* 获得此博客文章的评论
*/
public function comments()
{
return $this->hasMany('App\Comment');
}
}
和前面一样,默认情况下,Eloquent
会使用父模型名的「蛇形命名」加上 _id
后缀作为外键字段。这里,Comment
模型对应到 Post
模型上的外键字段是 post_id
。
定义好关联之后,我们就可以通过 Eloquent
提供的动态属性 comments
来访问评论的集合了:
$comments = App\Post::find(1)->comments;
foreach ($comments as $comment) {
//
}
当然,由于所有的关联还可以作为查询构造器使用,还可以使用链式调用的方式,在 comments
方法后面添加更多的约束条件:
$comments = App\Post::find(1)->comments()->where('title', 'foo')->first();
和 hasOne
方法一样,也可以通过传递参数来覆盖默认的外键名和本地键名:
return $this->hasMany('App\Comment', 'foreigh_key');
return $this->hasMany('App\Comment', 'foreign_key', 'local_key');
一对多反向关联
既然我们已经能获得一篇文章的所有评论,让我们接着再定义一个关联,来通过评论获得父模型文章。在子模型中使用 belongsTo
方法定义它:
namespace App;
use Illuminate\Database\Eloquent\Model;
class Comment extends Model
{
/**
* 获得此评论所属的文章
*/
public function post()
{
return $this->belongsTo('App\Post');
}
}
定义好关联,就可以在 Comment
模型上使用动态属性 post
来获取 Post
模型了。
$comment = App\Comment::find(1);
echo $comment->post->title;
如果 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');
}
2.3 多对多
多对多比一对一和一对多稍微复杂一点。举个例子,一个用户拥有多种角色,同时这些角色也被其他用户共享。例如,许多用户都可以有「管理员」角色。要定义这种关联,需要用到三张数据表:users
,roles
和 role_user
。role_user
表的命名是由关联的两个模型名按照字母顺序而来的,并且包含了 user_id
和 role_id
字段。
通过在方法内返回 belongsToMany
的结果来定义多对多关联,我们在 User
模型中定义 roles
方法:
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 会按照字母顺序连接两个关联模型的名称。当然,也可以通过传递第二个参数来指定表名:
return $this->belongsToMany('App\Role', 'role_user');
除了指定中间表的表名外,还可以通过给 belongsToMany
方法传递其它参数来指定中间表的键名。第三个参数用来指定关联的模型在中间表里的外键名,第四个参数是另一个模型在中间表里的外键名:
return $this->belongsToMany('App\Role', 'role_user', 'user_id', 'role_id');
多对多反向关联
要定义多对多的反向关联,只需要在对方模型里面再次调用 belongsToMany
方法就可以了。我们继续在 Role
模型里添加 users
方法:
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
对象只包含两个关键模型的键。如果中间表里还有额外字段,必须在定义关联时明确指出:
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
类。例如,我们在定义 Role
模型的关联时,使用自定义中间表模型 UserRole
:
namespace App;
use Illuminate\Database\Eloquent\Model;
class Role extends Model
{
/**
* 获得拥有该角色的用户
*/
public function users()
{
return $this->belongsToMany('App\User')->using('App\UserRole');
}
}
UserRole
模型继承自 Pivot
类:
namespace App;
use Illuminate\Database\Eloquent\Relations\Pivot;
class UserRole extends Pivot
{
//
}
2.4 远程一对多
远程一对多提供了方便、简短的方式,通过中间表来获得远程的关联。例如,一个 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
模型中定义它:
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', // 国家表本地键...
'id' // 用户表本地键...
);
}
}
2.5 多态一对多
数据表结构
多态关联允许一个模型属于多个其它模型。例如,用户可以评论文章和视频。使用多态关联,可以使用一张 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
用来保存文章或者视频的 ID 值,而 commentable_type
用来保存所属模型的类名。commentable_type
用来在访问 commentable
关联时,让 ORM 知道父模型具体是哪个类。
模型结构
接下来,我们看看建立该关联所需的模型定义:
namespace App;
use Illuminate\Database\Eloquent\Model;
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');
}
}
class Comment extends Model
{
/**
* 获得拥有此评论的模型
*/
public function commentable()
{
return $this->morphTo();
}
}
获取多态关联
我们只需要简单地使用 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
实例,具体取决于评论所属的模型类型。
自定义多态关联的类型字段
默认情况下,Laravel 会将关联模型的完全限定类名保存为关联模型类型。比如,在上例中,Comment
属于 Post
或者 Video
,那么 commentable_type
的默认值分别对应的就是 App\Post
和 App\Video
。如果希望将数据库与程序内部结构解耦,可以自定义一个「多态映射表」来指定 Eloquent 使用的字段名而不是类名:
use Illuminate\Database\Eloquent\Relations\Relation;
Relation::morphMap([
'posts' => 'App\Post',
'videos' => 'App\Video',
]);
可以在 AppServiceProvider
中的 boot
函数中使用 Relation:morphMap
方法注册「多态映射表」,或者使用一个单独的服务提供者进行注册。
2.6 多态多对多
数据表结构
除了传统的多态关联,也可以定义多对多的多态关联。例如,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
方法:
namespace App;
use Illuminate\Database\Eloquent\Model;
class Post extends Model
{
/**
* 获得该文章的所有标签
*/
public function tags()
{
return $this->morphToMany('App\Tag', 'taggable');
}
}
多态多对多反向关联
下一步,在 Tag
模型中,为每个关联模型定义一个方法。所以,在本例中,我们要定义一个 posts
和一个 videos
方法:
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) {
//
}
2. 查询关联
由于所有类型的关联都通过方法定义,你可以调用这些方法来获取关联实例,而不需要实际运行关联查询。此外,所有类型的关联都可以作为查询构造器使用,即可以在向数据库执行 SQL 语句前,使用链式调用添加约束条件。
例如,在一个博客系统,User
模型有多个关联的 Post
模型:
namespace App;
use Illuminate\Database\Eloquent\Model;
class User extends Model
{
/**
* 获得此用户所有的文章
*/
public function posts()
{
return $this->hasMany('App\Post');
}
}
可以像这样在 posts
关联上添加额外约束条件:
$user = App\User::find(1);
$user->posts()->where('active', 1)->get();
在关联上可以使用查询构造器的任意方法。
2.1 关联方法 Vs. 动态属性
如果不需要给 Eloquent 关联查询添加额外的约束条件,可以像访问属性一样访问关联。例如,在刚刚的 User
和 Post
模型例子中,我们可以这样访问一个用户的所有文章:
$user = App\User::find(1);
foreach ($user->posts as $post) {
//
}
动态属性是「懒加载」的,意味着只有在被实际访问时才加载关联数据。因此,开发者经常使用「预加载」提前加载之后会用到的关联数据。「预加载」有效地减少了 SQL 请求数,避免了重复执行一个模型加载关联数据、发送 SQL 请求带来的性能问题。
2.2 基于已存在的关联进行查询
当获取模型记录时,可能希望基于存在的关联对结果进行约束。例如,想获得至少有一条评论的所有博客文章。为了实现该功能,可以给 has
或 orHas
方法传递关联名称:
// 获得至少有一条评论的所有文章
$posts = App\Post::has('comments')->get();
也可以指定运算符和数量,进一步自定义查询:
// 获得有三条或三条以上评论的所有文章
$posts = Post::has('comments', '>=', 3)->get();
还可以使用「点」语法构造嵌套的 has
语句。例如,获得至少有一条获赞评论的所有文章:
// 获得至少有一条获赞评论的所有文章
$posts = Post::has('comments.votes')->get();
如果需要更高级的用法,可以使用 whereHas
和 orWhereHas
方法指定查询条件。例如对评论内容进行检查:
// 获得至少有一条评论内容满足 foo% 条件的所有文章
$posts = Post::whereHas('comments', function ($query) {
$query->where('content', 'like', 'foo%');
})->get();
2.3 基于不存在的关联进行查询
当获取模型记录时,也可能希望基于不存在的关联对结果进行约束。例如,要获得没有任何评论的所有博客文章。为了实现该功能,可以给 doesntHave
或 orDoesntHave
方法传递关联名称:
$posts = App\Post::doesntHave('comments')->get();
如果需要更高级的用法,可以使用 whereDoesntHave
或者 orWhereDoesntHave
方法指定查询条件。例如对评论内容进行检查:
$posts = Post::whereDoesntHave('comments', function ($query) {
$query->where('content', 'like', 'foo%');
})->get();
还可以使用「点」语法构造嵌套的 doesntHave
语句。例如,获得包含未禁用用户评论的所有文章:
$posts = App\Post::whereDoesntHave('comments.author', function ($query) {
$query->where('banned', 1);
})->get();
2.4 关联计数
如果只是想统计结果集的数量而不需要这些数据时,可以使用 withCount
方法,它会在结果集模型中添加一个 {关联名}_count
字段。例如:
$posts = App\Post::withCount('comments')->get();
foreach ($posts as $post) {
echo $post->comments_count;
}
可以同时获取多个关联计数,并为其添加查询约束条件:
$posts = App\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_count' => function ($query) {
$query->where('approved', false);
}
])->get();
echo $posts[0]->comments_count;
echo $posts[0]->pending_comments_count;
3. 预加载
当用属性访问模型关联时,关联的数据是「懒加载」的。意味着关联数据只会在初次访问该属性时才被加载。在查询父模型时,Eloquent 可以「预加载」关联数据。预加载避免了 N+1 次查询的问题。以 Book
和 Author
模型关联为例,说明 N+1 查询的问题:
namespace App;
use Illuminate\Database\Eloquent\Model;
class Book extends Model
{
/**
* 获取书的作者
*/
public function author()
{
return $this->belongsTo('App\Author');
}
}
现在,我们来获取所有书籍和书作者的数据:
$books = App\Book::all();
foreach ($books as $book) {
echo $book->author->name;
}
这个循环将会执行一条语句去数据表查询所有的书籍信息,然后每本书执行另一条语句去获取作者信息。因此,如果我们有 25 本书,这个循环就会执行 26 条查询语句,一条获取书籍信息,另外 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, ...);
3.1 预加载多个关联
有时候,可能需要在一次操作中预加载多个不同的关联。只需要给 with
方法传递额外的参数就能实现:
$books = App\Book::with(['author', 'publisher'])->get();
3.2 嵌套预加载
可以使用「点」语法来嵌套预加载。例如,在一个 Eloquent 声明中,预加载所有书籍的作者和这些作者的个人联系信息:
$books = App\Book::with('author.contacts')->get();
3.3 预加载指定列
可能不总是需要从关联中获取每一列数据,为此,Eloquent 也允许在关联中指定要查询的列:
$users = App\Book::with('author:id,name')->get();
使用该功能时,需要在获取的列中始终包含
id
列。
3.4 约束预加载
在使用预加载时,有时需要进行额外的约束。如下:
$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();
3.5 延迟预加载
有时候,需要在已查询出来的模型上进行预加载。这在动态决定是否进行预加载时很有帮助:
$books = App\Book::all();
if ($someCondition) {
$books->load('author', 'publisher');
}
如果需要在预加载上添加额外的约束,可以传入一个数组,关联作为数组的键,值是一个接收查询实例的闭包:
$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
];
}
4. 插入 & 更新关联模型
4.1 保存
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.']),
]);
递归保存模型和模型关联
如果要保存模型以及所有的模型关联,可以使用 push
方法:
$post = App\Post::find(1);
$post->comments[0]->message = 'Message';
$post->comments[0]->author->name = 'Author Name';
$post->push();
4.2 新增
除了 save
和 saveMany
方法外,还可以使用接收属性数组的 create
方法。它会创建模型,并插入到数据库中。save
和 create
方法的区别在于,save
方法接收一个完整的 Eloquent 模型实例,而 create
则接收一个数组:
$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.',
],
]);
4.3 更新 belongsTo
关联
当更新 belongsTo
关联时,可以使用 associate
方法。该方法会在子模型中设置外键:
$account = App\Account::find(10);
$user->account()->associate($account);
$user->save();
当移除 belongsTo
关联时,可以使用 dissociate
方法。该方法会将关联外键设置为 null
,但需要确保数据表中该字段可以为空:
$user->account()->dissociate();
$user->save();
注意此处的
belongsTo
关联,与关联类的类名是一一对应的。例如一个用户拥有多篇博客文章,dd($post->user());
看到该关联是一个belongsTo
类的实例,那么可以调用associate
或dissociate
方法将博客文章对象关联到用户,或者移除对应关联。
$user = Auth::user();
$post = Post::first();
$post->user()->associate($user);
$post->save();
默认模型
belongsTo
关联允许指定默认模型,当给定的关联为 null
时,将会返回默认模型。这种模式称为 空对象模式,可以用于在代码中省去条件判断。在下面的例子中,如果发布的帖子没有找到作者,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) {
$user->name = 'Guest Author';
});
}
4.4 多对多关联
附加 / 分离
Eloquent 也提供了一些额外的辅助方法,来更加方便地处理相关模型。例如,我们假设一个用户可以拥有多个角色,并且每个角色都可以属于多个用户。如果要关联模型,可以通过向中间表中新增一条记录来附加一个角色到用户,使用 attach
方法:
$user = App\User::find(1);
$user->roles()->attach($roleId);
在将关联附加到模型时,还可以传递一个要插入到中间表包含额外数据的数组:
$user->roles()->attach($roleId, ['expires' => $expires]);
当然,有时也需要移除用户的角色。可以使用 detach
移除多对多关联记录。detach
方法将会移除中间表对应的记录;但是这两个模型都将会保留在数据库中:
// 移除用户的一个角色
$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);
5. 更新父模型时间戳
当一个模型 belongsTo
或者 belongToMany
另一个模型,比如 Comment
属于 Post
,在子模型更新的同时更新父模型的时间戳时非常有用。例如,当 Comment
模型被更新时,自动「触发」父模型 Post
的 updated_at
时间戳的更新。在 Eloquent 中很容易实现。只要在子模型添加一个包含关联名称的 touches
属性即可。
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();