Eloquent: 关系
介绍
数据库表通常是相互关联的。例如,一篇博客文章可能有很多评论,或者一个订单可能与下单的用户相关。Eloquent 使得管理和处理这些关系变得简单,并支持多种不同类型的关系:
定义关系
Eloquent 关系被定义为 Eloquent 模型类上的方法。由于关系也像 Eloquent 模型一样,充当强大的查询构建器,将关系定义为方法提供了强大的方法链和查询功能。例如,我们可以在 posts
关系上链式添加额外的约束:
$user->posts()->where('active', 1)->get();
但在深入使用关系之前,让我们先学习如何定义每种类型。
一对一
一对一关系是非常基本的关系。例如,一个 User
模型可能与一个 Phone
相关联。要定义这种关系,我们在 User
模型上放置一个 phone
方法。phone
方法应调用 hasOne
方法并返回其结果:
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class User extends Model
{
/**
* 获取与用户关联的电话记录。
*/
public function phone()
{
return $this->hasOne('App\Phone');
}
}
传递给 hasOne
方法的第一个参数是相关模型的名称。一旦定义了关系,我们可以使用 Eloquent 的动态属性检索相关记录。动态属性允许您像访问模型上定义的属性一样访问关系方法:
$phone = User::find(1)->phone;
Eloquent 根据模型名称确定关系的外键。在这种情况下,Phone
模型被自动假定为具有 user_id
外键。如果您希望覆盖此约定,可以将第二个参数传递给 hasOne
方法:
return $this->hasOne('App\Phone', 'foreign_key');
此外,Eloquent 假定外键应具有与父级的 id
(或自定义 $primaryKey
)列匹配的值。换句话说,Eloquent 将在 Phone
记录的 user_id
列中查找用户的 id
列的值。如果您希望关系使用 id
以外的值,可以将第三个参数传递给 hasOne
方法指定您的自定义键:
return $this->hasOne('App\Phone', 'foreign_key', 'local_key');
定义关系的反向
因此,我们可以从 User
访问 Phone
模型。现在,让我们在 Phone
模型上定义一个关系,以便我们可以访问拥有电话的 User
。我们可以使用 belongsTo
方法定义 hasOne
关系的反向:
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Phone extends Model
{
/**
* 获取拥有电话的用户。
*/
public function user()
{
return $this->belongsTo('App\User');
}
}
在上面的示例中,Eloquent 将尝试将 Phone
模型中的 user_id
与 User
模型中的 id
匹配。Eloquent 通过检查关系方法的名称并在方法名称后加上 _id
来确定默认的外键名称。但是,如果 Phone
模型上的外键不是 user_id
,您可以将自定义键名作为第二个参数传递给 belongsTo
方法:
/**
* 获取拥有电话的用户。
*/
public function user()
{
return $this->belongsTo('App\User', 'foreign_key');
}
如果您的父模型不使用 id
作为其主键,或者您希望将子模型连接到不同的列,可以将第三个参数传递给 belongsTo
方法指定您的父表的自定义键:
/**
* 获取拥有电话的用户。
*/
public function user()
{
return $this->belongsTo('App\User', 'foreign_key', 'other_key');
}
一对多
"一对多" 关系用于定义一个模型拥有任意数量其他模型的关系。例如,一篇博客文章可能有无限数量的评论。像所有其他 Eloquent 关系一样,一对多关系通过在 Eloquent 模型上放置一个函数来定义:
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Post extends Model
{
/**
* 获取博客文章的评论。
*/
public function comments()
{
return $this->hasMany('App\Comment');
}
}
请记住,Eloquent 将自动确定 Comment
模型上的正确外键列。按照惯例,Eloquent 将采用拥有模型的 "蛇形命名" 并在其后加上 _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
方法的关系函数:
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Comment extends Model
{
/**
* 获取拥有评论的文章。
*/
public function post()
{
return $this->belongsTo('App\Post');
}
}
一旦定义了关系,我们可以通过访问 post
"动态属性" 来检索 Comment
的 Post
模型:
$comment = App\Comment::find(1);
echo $comment->post->title;
在上面的示例中,Eloquent 将尝试将 Comment
模型中的 post_id
与 Post
模型中的 id
匹配。Eloquent 通过检查关系方法的名称并在方法名称后加上 _
和主键列的名称来确定默认的外键名称。但是,如果 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
方法:
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class User extends Model
{
/**
* 属于用户的角色。
*/
public function roles()
{
return $this->belongsToMany('App\Role');
}
}
一旦定义了关系,您可以使用 roles
动态属性访问用户的角色:
$user = App\User::find(1);
foreach ($user->roles as $role) {
//
}
当然,像所有其他关系类型一样,您可以调用 roles
方法以继续在关系上链式添加查询约束:
$roles = App\User::find(1)->roles()->orderBy('name')->get();
如前所述,为了确定关系连接表的表名,Eloquent 将按字母顺序连接两个相关模型名称。但是,您可以自由地覆盖此约定。您可以通过将第二个参数传递给 belongsToMany
方法来实现:
return $this->belongsToMany('App\Role', 'role_user');
除了自定义连接表的名称,您还可以通过传递额外的参数给 belongsToMany
方法来自定义表上键的列名。第三个参数是您定义关系的模型的外键名,而第四个参数是您要连接的模型的外键名:
return $this->belongsToMany('App\Role', 'role_user', 'user_id', 'role_id');
定义关系的反向
要定义多对多关系的反向,您可以在相关模型上放置另一个 belongsToMany
调用。为了继续我们的用户角色示例,让我们在 Role
模型上定义 users
方法:
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Role extends Model
{
/**
* 属于角色的用户。
*/
public function users()
{
return $this->belongsToMany('App\User');
}
}
如您所见,关系的定义与其 User
对应关系完全相同,除了引用 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
对象上。如果您的中间表包含额外的属性,您必须在定义关系时指定它们:
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
类。例如,我们可以定义一个使用自定义 UserRole
枢纽模型的 Role
:
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Role extends Model
{
/**
* 属于角色的用户。
*/
public function users()
{
return $this->belongsToMany('App\User')->using('App\UserRole');
}
}
在定义 UserRole
模型时,我们将扩展 Pivot
类:
<?php
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
模型上定义它:
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Country extends Model
{
/**
* 获取国家的所有文章。
*/
public function posts()
{
return $this->hasManyThrough('App\Post', 'App\User');
}
}
传递给 hasManyThrough
方法的第一个参数是我们希望访问的最终模型的名称,而第二个参数是中间模型的名称。
在执行关系查询时,将使用典型的 Eloquent 外键约定。如果您希望自定义关系的键,可以将它们作为第三个和第四个参数传递给 hasManyThrough
方法。第三个参数是中间模型上的外键名称。第四个参数是最终模型上的外键名称。第五个参数是本地键,而第六个参数是中间模型的本地键:
class Country extends Model
{
public function posts()
{
return $this->hasManyThrough(
'App\Post',
'App\User',
'country_id', // 用户表上的外键...
'user_id', // 文章表上的外键...
'id', // 国家表上的本地键...
'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
关系时确定返回哪种 "类型" 的拥有模型的方式。
模型结构
接下来,让我们检查构建此关系所需的模型定义:
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Comment extends Model
{
/**
* 获取所有拥有的 commentable 模型。
*/
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
。但是,您可能希望将数据库与应用程序的内部结构解耦。在这种情况下,您可以定义关系 "morph map" 来指示 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
方法:
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Post extends Model
{
/**
* 获取文章的所有标签。
*/
public function tags()
{
return $this->morphToMany('App\Tag', 'taggable');
}
}
定义关系的反向
接下来,在 Tag
模型上,您应该为每个相关模型定义一个方法。因此,对于此示例,我们将定义一个 posts
方法和一个 videos
方法:
<?php
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
模型:
<?php
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();
您可以在关系上使用任何查询构建器方法,因此请务必查看查询构建器文档以了解所有可用的方法。
关系方法与动态属性
如果您不需要向 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
方法在 has
查询上添加 "where" 条件。这些方法允许您向关系约束添加自定义约束,例如检查评论的内容:
// 检索所有至少有一个包含类似 foo% 的评论的文章
$posts = App\Post::whereHas('comments', function ($query) {
$query->where('content', 'like', 'foo%');
})->get();
查询关系不存在
在访问模型的记录时,您可能希望根据关系的不存在限制结果。例如,想象您想要检索所有没有评论的博客文章。为此,您可以将关系的名称传递给 doesntHave
和 orDoesntHave
方法:
$posts = App\Post::doesntHave('comments')->get();
如果您需要更强大的功能,可以使用 whereDoesntHave
和 orWhereDoesntHave
方法在 doesntHave
查询上添加 "where" 条件。这些方法允许您向关系约束添加自定义约束,例如检查评论的内容:
$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 查询问题,考虑一个与 Author
相关的 Book
模型:
<?php
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;
}
对于此操作,将仅执行两个查询:
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 只会预加载标题列包含单词 first
的文章。当然,您可以调用其他查询构建器方法以进一步自定义预加载操作:
$users = App\User::with(['posts' => function ($query) {
$query->orderBy('created_at', 'desc');
}])->get();
延迟预加载
有时您可能需要在父模型已经检索到之后预加载关系。例如,如果您需要动态决定是否加载相关模型,这可能很有用:
$books = App\Book::all();
if ($someCondition) {
$books->load('author', 'publisher');
}
如果您需要在预加载查询上设置额外的查询约束,可以传递一个以您希望加载的关系为键的数组。数组值应为接收查询实例的 Closure
实例:
$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
。而不是手动设置 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
方法将自动向新 Comment
模型添加适当的 post_id
值。
如果您需要保存多个相关模型,可以使用 saveMany
方法:
$post = App\Post::find(1);
$post->comments()->saveMany([
new App\Comment(['message' => 'A new comment.']),
new App\Comment(['message' => 'Another comment.']),
]);
Create 方法
除了 save
和 saveMany
方法,您还可以使用 create
方法,该方法接受一个属性数组,创建一个模型,并将其插入到数据库中。同样,save
和 create
的区别在于 save
接受一个完整的 Eloquent 模型实例,而 create
接受一个普通的 PHP array
:
$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.',
],
]);
更新 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
关系将返回一个空的 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
的 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();