Eloquent:关系
介绍
数据库表通常彼此相关。例如,博客文章可能有许多评论,或者订单可能与下订单的用户相关。Eloquent 使管理和处理这些关系变得容易,并支持多种常见关系:
定义关系
Eloquent 关系在您的 Eloquent 模型类上定义为方法。由于关系也充当强大的 查询构建器,将关系定义为方法提供了强大的方法链和查询功能。例如,我们可以在此 posts
关系上链接其他查询约束:
$user->posts()->where('active', 1)->get();
但是,在深入使用关系之前,让我们学习如何定义 Eloquent 支持的每种类型的关系。
一对一
一对一关系是一种非常基本的数据库关系。例如,User
模型可能与一个 Phone
模型相关联。要定义此关系,我们将在 User
模型上放置一个 phone
方法。phone
方法应调用 hasOne
方法并返回其结果。hasOne
方法通过模型的 Illuminate\Database\Eloquent\Model
基类提供给您的模型:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasOne;
class User extends Model
{
/**
* 获取与用户关联的电话。
*/
public function phone(): HasOne
{
return $this->hasOne(Phone::class);
}
}
传递给 hasOne
方法的第一个参数是相关模型类的名称。一旦定义了关系,我们可以使用 Eloquent 的动态属性检索相关记录。动态属性允许您像访问模型上定义的属性一样访问关系方法:
$phone = User::find(1)->phone;
Eloquent 根据父模型名称确定关系的外键。在这种情况下,Phone
模型被自动假定具有 user_id
外键。如果您希望覆盖此约定,可以将第二个参数传递给 hasOne
方法:
return $this->hasOne(Phone::class, 'foreign_key');
此外,Eloquent 假定外键应具有与父级的主键列匹配的值。换句话说,Eloquent 将在 Phone
记录的 user_id
列中查找用户的 id
列的值。如果您希望关系使用 id
或模型的 $primaryKey
属性以外的主键值,可以将第三个参数传递给 hasOne
方法:
return $this->hasOne(Phone::class, 'foreign_key', 'local_key');
定义关系的反向
因此,我们可以从 User
模型访问 Phone
模型。接下来,让我们在 Phone
模型上定义一个关系,以便我们可以访问拥有电话的用户。我们可以使用 belongsTo
方法定义 hasOne
关系的反向关系:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class Phone extends Model
{
/**
* 获取拥有电话的用户。
*/
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
}
调用 user
方法时,Eloquent 将尝试查找 User
模型,该模型具有与 Phone
模型上的 user_id
列匹配的 id
。
Eloquent 通过检查关系方法的名称并在方法名称后加上 _id
后缀来确定外键名称。因此,在这种情况下,Eloquent 假定 Phone
模型具有 user_id
列。但是,如果 Phone
模型上的外键不是 user_id
,您可以将自定义键名作为第二个参数传递给 belongsTo
方法:
/**
* 获取拥有电话的用户。
*/
public function user(): BelongsTo
{
return $this->belongsTo(User::class, 'foreign_key');
}
如果父模型不使用 id
作为其主键,或者您希望使用其他列查找关联模型,可以将第三个参数传递给 belongsTo
方法,指定父表的自定义键:
/**
* 获取拥有电话的用户。
*/
public function user(): BelongsTo
{
return $this->belongsTo(User::class, 'foreign_key', 'owner_key');
}
一对多
一对多关系用于定义单个模型是一个或多个子模型的父模型的关系。例如,博客文章可能有无限数量的评论。与所有其他 Eloquent 关系一样,一对多关系通过在 Eloquent 模型上定义方法来定义:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
class Post extends Model
{
/**
* 获取博客文章的评论。
*/
public function comments(): HasMany
{
return $this->hasMany(Comment::class);
}
}
请记住,Eloquent 将自动确定 Comment
模型的正确外键列。按照惯例,Eloquent 将采用父模型的 "蛇形命名法" 名称,并在其后加上 _id
。因此,在此示例中,Eloquent 将假定 Comment
模型上的外键列为 post_id
。
一旦定义了关系方法,我们可以通过访问 comments
属性来访问相关评论的 集合。请记住,由于 Eloquent 提供了 "动态关系属性",我们可以像在模型上定义的属性一样访问关系方法:
use App\Models\Post;
$comments = Post::find(1)->comments;
foreach ($comments as $comment) {
// ...
}
由于所有关系也充当查询构建器,您可以通过调用 comments
方法并继续将条件链接到查询来为关系查询添加更多约束:
$comment = Post::find(1)->comments()
->where('title', 'foo')
->first();
与 hasOne
方法一样,您还可以通过向 hasMany
方法传递其他参数来覆盖外键和本地键:
return $this->hasMany(Comment::class, 'foreign_key');
return $this->hasMany(Comment::class, 'foreign_key', 'local_key');
一对多(反向)/ 属于
现在我们可以访问所有帖子的评论,让我们定义一个关系,以便评论可以访问其父帖子。要定义 hasMany
关系的反向关系,请在子模型上定义一个调用 belongsTo
方法的关系方法:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class Comment extends Model
{
/**
* 获取拥有评论的帖子。
*/
public function post(): BelongsTo
{
return $this->belongsTo(Post::class);
}
}
一旦定义了关系,我们可以通过访问 post
"动态关系属性" 来检索评论的父帖子:
use App\Models\Comment;
$comment = Comment::find(1);
return $comment->post->title;
在上面的示例中,Eloquent 将尝试查找 Post
模型,该模型具有与 Comment
模型上的 post_id
列匹配的 id
。
Eloquent 通过检查关系方法的名称并在方法名称后加上 _
和父模型的主键列的名称来确定默认的外键名称。因此,在此示例中,Eloquent 将假定 Post
模型在 comments
表上的外键为 post_id
。
但是,如果关系的外键不遵循这些约定,您可以将自定义外键名称作为第二个参数传递给 belongsTo
方法:
/**
* 获取拥有评论的帖子。
*/
public function post(): BelongsTo
{
return $this->belongsTo(Post::class, 'foreign_key');
}
如果父模型不使用 id
作为其主键,或者您希望使用其他列查找关联模型,可以将第三个参数传递给 belongsTo
方法,指定父表的自定义键:
/**
* 获取拥有评论的帖子。
*/
public function post(): BelongsTo
...(about 236 lines omitted)...
Model::handleLazyLoadingViolationUsing(function (Model $model, string $relation) {
$class = $model::class;
info("Attempted to lazy load [{$relation}] on model [{$class}].");
});
插入和更新相关模型
save
方法
Eloquent 提供了方便的方法来向关系中添加新模型。例如,您可能需要向帖子添加新评论。与其手动在 Comment
模型上设置 post_id
属性,您可以使用关系的 save
方法插入评论:
use App\Models\Comment;
use App\Models\Post;
$comment = new Comment(['message' => 'A new comment.']);
$post = Post::find(1);
$post->comments()->save($comment);
请注意,我们没有将 comments
关系作为动态属性访问。相反,我们调用了 comments
方法以获取关系的实例。save
方法将自动将适当的 post_id
值添加到新的 Comment
模型。
如果您需要保存多个相关模型,可以使用 saveMany
方法:
$post = Post::find(1);
$post->comments()->saveMany([
new Comment(['message' => 'A new comment.']),
new Comment(['message' => 'Another new comment.']),
]);
save
和 saveMany
方法将持久化给定的模型实例,但不会将新持久化的模型添加到已加载到父模型上的任何内存关系中。如果您计划在使用 save
或 saveMany
方法后访问关系,您可能希望使用 refresh
方法重新加载模型及其关系:
$post->comments()->save($comment);
$post->refresh();
// 所有评论,包括新保存的评论...
$post->comments;
递归保存模型和关系
如果您希望 save
模型及其所有关联关系,可以使用 push
方法。在此示例中,将保存 Post
模型及其评论和评论的作者:
$post = Post::find(1);
$post->comments[0]->message = 'Message';
$post->comments[0]->author->name = 'Author Name';
$post->push();
pushQuietly
方法可用于在不触发任何事件的情况下保存模型及其关联关系:
$post->pushQuietly();
create
方法
除了 save
和 saveMany
方法外,您还可以使用 create
方法,该方法接受属性数组,创建模型并将其插入数据库。save
和 create
的区别在于,save
接受完整的 Eloquent 模型实例,而 create
接受普通的 PHP array
。create
方法将返回新创建的模型:
use App\Models\Post;
$post = Post::find(1);
$comment = $post->comments()->create([
'message' => 'A new comment.',
]);
您可以使用 createMany
方法创建多个相关模型:
$post = Post::find(1);
$post->comments()->createMany([
['message' => 'A new comment.'],
['message' => 'Another new comment.'],
]);
createQuietly
和 createManyQuietly
方法可用于在不触发任何事件的情况下创建模型:
$user = User::find(1);
$user->posts()->createQuietly([
'title' => 'Post title.',
]);
$user->posts()->createManyQuietly([
['title' => 'First post.'],
['title' => 'Second post.'],
]);
您还可以使用 findOrNew
、firstOrNew
、firstOrCreate
和 updateOrCreate
方法在关系上 创建和更新模型。
在使用 create
方法之前,请务必查看 批量赋值 文档。
属于关系
如果您希望将子模型分配给新的父模型,可以使用 associate
方法。在此示例中,User
模型定义了一个 belongsTo
关系到 Account
模型。此 associate
方法将设置子模型上的外键:
use App\Models\Account;
$account = Account::find(10);
$user->account()->associate($account);
$user->save();
要从子模型中移除父模型,可以使用 dissociate
方法。此方法将关系的外键设置为 null
:
$user->account()->dissociate();
$user->save();
多对多关系
附加/分离
Eloquent 还提供了使处理多对多关系更方便的方法。例如,假设用户可以有多个角色,角色可以有多个用户。您可以使用 attach
方法通过在关系的中间表中插入记录来将角色附加到用户:
use App\Models\User;
$user = 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 = 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 插入相同的中间表值,可以使用 syncWithPivotValues
方法:
$user->roles()->syncWithPivotValues([1, 2, 3], ['active' => true]);
如果您不想分离给定数组中缺失的现有 ID,可以使用 syncWithoutDetaching
方法:
$user->roles()->syncWithoutDetaching([1, 2, 3]);
切换关联
多对多关系还提供了一个 toggle
方法,该方法 "切换" 给定相关模型 ID 的附加状态。如果给定 ID 当前已附加,它将被分离。同样,如果当前已分离,它将被附加:
$user->roles()->toggle([1, 2, 3]);
您还可以与 ID 一起传递其他中间表值:
$user->roles()->toggle([
1 => ['expires' => true],
2 => ['expires' => true],
]);
更新中间表上的记录
如果您需要更新关系的中间表中的现有行,可以使用 updateExistingPivot
方法。此方法接受中间记录外键和要更新的属性数组:
$user = User::find(1);
$user->roles()->updateExistingPivot($roleId, [
'active' => false,
]);
触摸父级时间戳
当模型定义了 belongsTo
或 belongsToMany
关系到另一个模型时,例如 Comment
属于 Post
,在子模型更新时更新父级的时间戳有时很有帮助。
例如,当更新 Comment
模型时,您可能希望自动 "触摸" 拥有的 Post
的 updated_at
时间戳,以便将其设置为当前日期和时间。为此,您可以在子模型中添加一个 touches
属性,其中包含在子模型更新时应更新其 updated_at
时间戳的关系名称:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class Comment extends Model
{
/**
* 所有应触摸的关系。
*
* @var array
*/
protected $touches = ['post'];
/**
* 获取评论所属的帖子。
*/
public function post(): BelongsTo
{
return $this->belongsTo(Post::class);
}
}
只有在使用 Eloquent 的 save
方法更新子模型时,才会更新父模型的时间戳。