Skip to content

Eloquent:关系

介绍

数据库表通常彼此相关。例如,博客文章可能有许多评论,或者订单可能与下订单的用户相关。Eloquent 使管理和处理这些关系变得容易,并支持多种常见关系:

定义关系

Eloquent 关系在您的 Eloquent 模型类上定义为方法。由于关系也充当强大的 查询构建器,将关系定义为方法提供了强大的方法链和查询功能。例如,我们可以在此 posts 关系上链接其他查询约束:

php
$user->posts()->where('active', 1)->get();

但是,在深入使用关系之前,让我们学习如何定义 Eloquent 支持的每种类型的关系。

一对一

一对一关系是一种非常基本的数据库关系。例如,User 模型可能与一个 Phone 模型相关联。要定义此关系,我们将在 User 模型上放置一个 phone 方法。phone 方法应调用 hasOne 方法并返回其结果。hasOne 方法通过模型的 Illuminate\Database\Eloquent\Model 基类提供给您的模型:

php
<?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 的动态属性检索相关记录。动态属性允许您像访问模型上定义的属性一样访问关系方法:

php
$phone = User::find(1)->phone;

Eloquent 根据父模型名称确定关系的外键。在这种情况下,Phone 模型被自动假定具有 user_id 外键。如果您希望覆盖此约定,可以将第二个参数传递给 hasOne 方法:

php
return $this->hasOne(Phone::class, 'foreign_key');

此外,Eloquent 假定外键应具有与父级的主键列匹配的值。换句话说,Eloquent 将在 Phone 记录的 user_id 列中查找用户的 id 列的值。如果您希望关系使用 id 或模型的 $primaryKey 属性以外的主键值,可以将第三个参数传递给 hasOne 方法:

php
return $this->hasOne(Phone::class, 'foreign_key', 'local_key');

定义关系的反向

因此,我们可以从 User 模型访问 Phone 模型。接下来,让我们在 Phone 模型上定义一个关系,以便我们可以访问拥有电话的用户。我们可以使用 belongsTo 方法定义 hasOne 关系的反向关系:

php
<?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 方法:

php
/**
 * 获取拥有电话的用户。
 */
public function user(): BelongsTo
{
    return $this->belongsTo(User::class, 'foreign_key');
}

如果父模型不使用 id 作为其主键,或者您希望使用其他列查找关联模型,可以将第三个参数传递给 belongsTo 方法,指定父表的自定义键:

php
/**
 * 获取拥有电话的用户。
 */
public function user(): BelongsTo
{
    return $this->belongsTo(User::class, 'foreign_key', 'owner_key');
}

一对多

一对多关系用于定义单个模型是一个或多个子模型的父模型的关系。例如,博客文章可能有无限数量的评论。与所有其他 Eloquent 关系一样,一对多关系通过在 Eloquent 模型上定义方法来定义:

php
<?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 提供了 "动态关系属性",我们可以像在模型上定义的属性一样访问关系方法:

php
use App\Models\Post;

$comments = Post::find(1)->comments;

foreach ($comments as $comment) {
    // ...
}

由于所有关系也充当查询构建器,您可以通过调用 comments 方法并继续将条件链接到查询来为关系查询添加更多约束:

php
$comment = Post::find(1)->comments()
                    ->where('title', 'foo')
                    ->first();

hasOne 方法一样,您还可以通过向 hasMany 方法传递其他参数来覆盖外键和本地键:

php
return $this->hasMany(Comment::class, 'foreign_key');

return $this->hasMany(Comment::class, 'foreign_key', 'local_key');

一对多(反向)/ 属于

现在我们可以访问所有帖子的评论,让我们定义一个关系,以便评论可以访问其父帖子。要定义 hasMany 关系的反向关系,请在子模型上定义一个调用 belongsTo 方法的关系方法:

php
<?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 "动态关系属性" 来检索评论的父帖子:

php
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 方法:

php
/**
 * 获取拥有评论的帖子。
 */
public function post(): BelongsTo
{
    return $this->belongsTo(Post::class, 'foreign_key');
}

如果父模型不使用 id 作为其主键,或者您希望使用其他列查找关联模型,可以将第三个参数传递给 belongsTo 方法,指定父表的自定义键:

php
/**
 * 获取拥有评论的帖子。
 */
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 方法插入评论:

php
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 方法:

php
$post = Post::find(1);

$post->comments()->saveMany([
    new Comment(['message' => 'A new comment.']),
    new Comment(['message' => 'Another new comment.']),
]);

savesaveMany 方法将持久化给定的模型实例,但不会将新持久化的模型添加到已加载到父模型上的任何内存关系中。如果您计划在使用 savesaveMany 方法后访问关系,您可能希望使用 refresh 方法重新加载模型及其关系:

php
$post->comments()->save($comment);

$post->refresh();

// 所有评论,包括新保存的评论...
$post->comments;

递归保存模型和关系

如果您希望 save 模型及其所有关联关系,可以使用 push 方法。在此示例中,将保存 Post 模型及其评论和评论的作者:

php
$post = Post::find(1);

$post->comments[0]->message = 'Message';
$post->comments[0]->author->name = 'Author Name';

$post->push();

pushQuietly 方法可用于在不触发任何事件的情况下保存模型及其关联关系:

php
$post->pushQuietly();

create 方法

除了 savesaveMany 方法外,您还可以使用 create 方法,该方法接受属性数组,创建模型并将其插入数据库。savecreate 的区别在于,save 接受完整的 Eloquent 模型实例,而 create 接受普通的 PHP arraycreate 方法将返回新创建的模型:

php
use App\Models\Post;

$post = Post::find(1);

$comment = $post->comments()->create([
    'message' => 'A new comment.',
]);

您可以使用 createMany 方法创建多个相关模型:

php
$post = Post::find(1);

$post->comments()->createMany([
    ['message' => 'A new comment.'],
    ['message' => 'Another new comment.'],
]);

createQuietlycreateManyQuietly 方法可用于在不触发任何事件的情况下创建模型:

php
$user = User::find(1);

$user->posts()->createQuietly([
    'title' => 'Post title.',
]);

$user->posts()->createManyQuietly([
    ['title' => 'First post.'],
    ['title' => 'Second post.'],
]);

您还可以使用 findOrNewfirstOrNewfirstOrCreateupdateOrCreate 方法在关系上 创建和更新模型

lightbulb

在使用 create 方法之前,请务必查看 批量赋值 文档。

属于关系

如果您希望将子模型分配给新的父模型,可以使用 associate 方法。在此示例中,User 模型定义了一个 belongsTo 关系到 Account 模型。此 associate 方法将设置子模型上的外键:

php
use App\Models\Account;

$account = Account::find(10);

$user->account()->associate($account);

$user->save();

要从子模型中移除父模型,可以使用 dissociate 方法。此方法将关系的外键设置为 null

php
$user->account()->dissociate();

$user->save();

多对多关系

附加/分离

Eloquent 还提供了使处理多对多关系更方便的方法。例如,假设用户可以有多个角色,角色可以有多个用户。您可以使用 attach 方法通过在关系的中间表中插入记录来将角色附加到用户:

php
use App\Models\User;

$user = User::find(1);

$user->roles()->attach($roleId);

在将关系附加到模型时,您还可以传递一个附加数据数组,以插入到中间表中:

php
$user->roles()->attach($roleId, ['expires' => $expires]);

有时可能需要从用户中移除角色。要移除多对多关系记录,请使用 detach 方法。detach 方法将从中间表中删除适当的记录;但是,两个模型将保留在数据库中:

php
// 从用户中分离单个角色...
$user->roles()->detach($roleId);

// 从用户中分离所有角色...
$user->roles()->detach();

为了方便起见,attachdetach 也接受 ID 数组作为输入:

php
$user = User::find(1);

$user->roles()->detach([1, 2, 3]);

$user->roles()->attach([
    1 => ['expires' => $expires],
    2 => ['expires' => $expires],
]);

同步关联

您还可以使用 sync 方法构建多对多关联。sync 方法接受一个要放置在中间表上的 ID 数组。任何不在给定数组中的 ID 将从中间表中移除。因此,在此操作完成后,只有给定数组中的 ID 将存在于中间表中:

php
$user->roles()->sync([1, 2, 3]);

您还可以与 ID 一起传递其他中间表值:

php
$user->roles()->sync([1 => ['expires' => true], 2, 3]);

如果您希望为每个同步的模型 ID 插入相同的中间表值,可以使用 syncWithPivotValues 方法:

php
$user->roles()->syncWithPivotValues([1, 2, 3], ['active' => true]);

如果您不想分离给定数组中缺失的现有 ID,可以使用 syncWithoutDetaching 方法:

php
$user->roles()->syncWithoutDetaching([1, 2, 3]);

切换关联

多对多关系还提供了一个 toggle 方法,该方法 "切换" 给定相关模型 ID 的附加状态。如果给定 ID 当前已附加,它将被分离。同样,如果当前已分离,它将被附加:

php
$user->roles()->toggle([1, 2, 3]);

您还可以与 ID 一起传递其他中间表值:

php
$user->roles()->toggle([
    1 => ['expires' => true],
    2 => ['expires' => true],
]);

更新中间表上的记录

如果您需要更新关系的中间表中的现有行,可以使用 updateExistingPivot 方法。此方法接受中间记录外键和要更新的属性数组:

php
$user = User::find(1);

$user->roles()->updateExistingPivot($roleId, [
    'active' => false,
]);

触摸父级时间戳

当模型定义了 belongsTobelongsToMany 关系到另一个模型时,例如 Comment 属于 Post,在子模型更新时更新父级的时间戳有时很有帮助。

例如,当更新 Comment 模型时,您可能希望自动 "触摸" 拥有的 Postupdated_at 时间戳,以便将其设置为当前日期和时间。为此,您可以在子模型中添加一个 touches 属性,其中包含在子模型更新时应更新其 updated_at 时间戳的关系名称:

php
<?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);
    }
}
exclamation

只有在使用 Eloquent 的 save 方法更新子模型时,才会更新父模型的时间戳。