Eloquent:关联
简介
数据表之间通常有一定的关联。例如,一篇博客文章可能有多条评论,或者一个订单对应一个下单用户。Eloquent 让我们更容易管理和使用这些关联,以下是支持的几种不同类型的关联:
定义关联
可以在 Eloquent 模型类上用方法定义 Eloquent 关联。因此,与 Eloquent 模型自身一样,关联也可以作为强大的 查询构造器 使用,用方法定义关联提供了强大的链式方法和查询功能。例如,我们可以在 posts
关联上链式添加其它约束:
$user->posts()->where('active', 1)->get();
但是,在深入使用关联之前,我们先来看看如何定义每种关联类型。
一对一
一对一关联是非常基础的关系。例如,一个 User
模型可能被关联到一个 Phone
。要定义此关联,可以在 User
模型上添加一个 phone
方法。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
外键。如果要覆盖此约定,可以传递第二个参数给 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
。可以使用 belongsTo
方法定义与 hasOne
相对的关联:
namespace App;
use Illuminate\Database\Eloquent\Model;
class Phone extends Model
{
/**
* 获取拥有此电话的用户
*/
public function user()
{
return $this->belongsTo('App\User');
}
}
在上述示例中,Eloquent 会尝试在 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 关联一样,通过在模型上添加一个函数定义一对多关联:
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
获取评论时继续在关联上链式添加其它约束:
$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
方法:
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 会尝试在 Post
模型的 id
上查找和 Comment
的 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
方法:
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
方法:
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
属性访问中间表属性。当然,您可以自由定义此属性名来更好地反映其在应用中的用途。
例如,如果应用中包含可能订阅播客的用户,那么用户和播客之间可能就是多对多关联。这种情况下,可能希望将中间表 pivot
属性重命名为 subscription
。可以在定义关联时使用 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
类。例如,我们定义一个使用自定义 UserRole
中间表模型的 Role
:
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
{
//
}
远程一对多
「远程一对多」关联为通过中间关联访问远程关联提供了方便的快捷操作。例如,一个 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 后,用它们查询 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' // 用户表的本地键
);
}
}
多态关联
数据表结构
多态关联允许一个模型在单个关联中属于多个其它模型。例如,假设应用中用户可以同时「评论」文章和视频。使用多态关联,可以使用单张 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
字段会在 ORM 获取 commentable
关联时用于确定返回哪个「类型」的父模型。
模型结构
接下来,我们查看创建此关联所需的模型定义:
namespace App;
use Illuminate\Database\Eloquent\Model;
class Comment extends Model
{
/**
* 获取所有可评论的父模型
*/
public function commentable()
{
return $this->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
实例,具体取决于拥有评论的模型类型。
自定义多态关联的类型字段
默认情况下,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
函数中注册 morphMap
,或者也可以创建一个单独的服务提供者。
多对多多态
数据表结构
除了传统的多态关联外,还可以定义「多对多」多态关联。例如,一个博客 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
方法:
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) {
//
}
查询关联
由于所有类型的 Eloquent 关联都通过方法定义,因此可以调用这些方法获取一个关联实例,而不用实际执行关联查询。此外,所有类型的 Eloquent 关联都可以作为 查询构造器 使用,所以可以在数据库最终运行 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();
可以在关联上使用任何 查询构造器 方法,因此务必查看查询构造器的文档了解有哪些可用的方法。
关联方法 Vs. 动态属性
如果不需要对 Eloquent 关联查询添加额外约束,可以获取关联就像它们是属性一样。例如,继续使用 User
和 Post
模型为例,我们可以像这样获取用户的所有文章:
$user = App\User::find(1);
foreach ($user->posts as $post) {
//
}
动态属性是「懒加载」,意味着只有在实际访问时它们才会加载其关联数据。因此,开发者经常使用「预加载」在加载模型后提前加载要使用的关联。预加载有效减少了加载模型关联必须执行的 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
查询。这些方法允许您将自定义约束添加到关联约束,例如检查一条评论的内容:
// 获取至少有一条以 foo 起头的评论的所有文章
$posts = App\Post::whereHas('comments', function ($query) {
$query->where('content', 'like', 'foo%');
})->get();
查询不存在关联
获取模型记录时,可能希望基于不存在的关联对结果进行限制。例如,假设希望获取没有任何评论的所有博客文章。要完成此操作,可以传递关联名称给 doesntHave
或 orDoesntHave
方法:
$posts = App\Post::doesntHave('comments')->get();
如果需要更强大的用法,可以使用 whereDoesntHave
和 orWhereDoesntHave
方法将「where」条件添加到 doesntHave
查询。这些方法允许您将自定义约束添加到关联约束,例如检查一条评论的内容:
$posts = App\Post::whereDoesntHave('comments', function ($query) {
$query->where('content', 'like', 'foo%');
})->get();
还可以使用「点」语法查询嵌套的关联。例如,下列查询会获取评论作者没有被禁用的所有文章:
$posts = App\Post::whereDoesntHave('comments.author', function ($query) {
$query->where('banned', 1);
})->get();
关联模型计数
如果要在不实际加载关联结果的情况下统计其数量,可以使用 withCount
方法,它会在结果模型中放一个 {relation}_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;
预加载
像属性一样获取 Eloquent 关联时,关联数据是「懒加载」。这意味着关联数据只会在初次访问属性时才会实际加载。但是,在查询父模型时 Eloquent 可以同时「预加载」关联。预加载避免了 N+1 查询问题。为了说明 N+1 查询问题,假设 Book
模型关联到了 Author
:
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;
}
此循环会执行 1 次查询获取数据表的所有书,然后每本书执行另一次查询获取作者。因此,如果我们有 25 本书,此循环会执行 26 次查询:1 次获取书,其它 25 次查询获取每本书的作者。
幸好,我们可以使用预加载将此操作减少到只有 2 次查询。在查询时,可以使用 with
方法指定要预加载的关联:
$books = App\Book::with('author')->get();
foreach ($books as $book) {
echo $book->author->name;
}
在此操作中,只会执行 2 次查询:
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();
预加载指定字段
获取关联时可能不总需要所有字段。因此,Eloquent 允许您指定要获取的关联字段:
$users = App\Book::with('author:id,name')->get();
使用此功能时,应该在要获取的字段列表中始终包括
id
字段。
约束预加载
有时可能希望预加载关联,但也希望为预加载查询指定其它查询约束。下面是一个示例:
$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();
延迟预加载
有时在获取父模型后可能需要预加载关联模型。例如,在需要动态决定是否加载关联模型时很有用:
$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
];
}
插入 & 更新关联模型
save
方法
Eloquent 为添加新模型到关联中提供了便捷的方法。例如,可能需要为 Post
模型插入一个新的 Comment
。可以直接使用关联的 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
模型和所有相关的关联,可以使用 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
关联允许您定义一个当给定关联为 null
时返回的默认模型。这种模式通常被称为 空对象模式,可以帮助在代码中省去条件判断。在以下示例中,如果没有 user
附加到文章则 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';
});
}
多对多关联
附加/分离
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 当前已附加,则会被分离。同样,如果当前已分离,则会被附加:
$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
的时间戳更新。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();