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 将尝试查找具有与 Phone 模型上的 user_id 列匹配的 idUser 模型。

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');

自动为子模型填充父模型

即使在使用 Eloquent 急切加载时,如果您尝试在循环遍历子模型时访问父模型,也可能会出现“N + 1”查询问题:

php
$posts = Post::with('comments')->get();

foreach ($posts as $post) {
    foreach ($post->comments as $comment) {
        echo $comment->post->title;
    }
}

在上面的示例中,由于即使为每个 Post 模型急切加载了评论,Eloquent 也不会自动为每个子 Comment 模型填充父 Post

如果您希望 Eloquent 自动将父模型填充到其子模型中,您可以在定义 hasMany 关系时调用 chaperone 方法:

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)->chaperone();
    }
}

或者,如果您希望在运行时选择性地启用自动父填充,您可以在急切加载关系时调用 chaperone 模型:

php
use App\Models\Post;

$posts = Post::with([
    'comments' => fn ($comments) => $comments->chaperone(),
])->get();

一对多(反向) / 属于

现在我们可以访问所有帖子的评论,让我们定义一个关系,以便评论可以访问其父帖子。要定义 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 将尝试查找具有与 Comment 模型上的 post_id 列匹配的 idPost 模型。

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
{
    return $this->belongsTo(Post::class, 'foreign_key', 'owner_key');
}

默认模型

belongsTohasOnehasOneThroughmorphOne 关系允许您定义一个默认模型,如果给定的关系为 null,则返回该模型。这种模式通常被称为 空对象模式,可以帮助消除代码中的条件检查。在以下示例中,user 关系将返回一个空的 App\Models\User 模型,如果没有用户附加到 Post 模型:

php
/**
 * 获取帖子的作者。
 */
public function user(): BelongsTo
{
    return $this->belongsTo(User::class)->withDefault();
}

要使用属性填充默认模型,您可以将数组或闭包传递给 withDefault 方法:

php
/**
 * 获取帖子的作者。
 */
public function user(): BelongsTo
{
    return $this->belongsTo(User::class)->withDefault([
        'name' => 'Guest Author',
    ]);
}

/**
 * 获取帖子的作者。
 */
public function user(): BelongsTo
{
    return $this->belongsTo(User::class)->withDefault(function (User $user, Post $post) {
        $user->name = 'Guest Author';
    });
}

查询属于关系

在查询“属于”关系的子项时,您可以手动构建 where 子句以检索相应的 Eloquent 模型:

php
use App\Models\Post;

$posts = Post::where('user_id', $user->id)->get();

但是,您可能会发现使用 whereBelongsTo 方法更方便,该方法将自动确定给定模型的正确关系和外键:

php
$posts = Post::whereBelongsTo($user)->get();

您还可以向 whereBelongsTo 方法提供一个 集合 实例。在这样做时,Laravel 将检索属于集合中任何父模型的模型:

php
$users = User::where('vip', true)->get();

$posts = Post::whereBelongsTo($users)->get();

默认情况下,Laravel 将根据给定模型的类名确定与给定模型相关的关系;但是,您可以通过将其作为第二个参数提供给 whereBelongsTo 方法手动指定关系名称:

php
$posts = Post::whereBelongsTo($user, 'author')->get();

拥有多个中的一个

有时,一个模型可能有多个相关模型,但您希望方便地检索用户下单的“最新”或“最旧”相关模型。例如,一个 User 模型可能与多个 Order 模型相关,但您希望定义一种方便的方式来与用户下单的最新订单进行交互。您可以使用 hasOne 关系类型结合 ofMany 方法来实现:

php
/**
 * 获取用户的最新订单。
 */
public function latestOrder(): HasOne
{
    return $this->hasOne(Order::class)->latestOfMany();
}

同样,您可以定义一个方法来检索“最旧”或第一个相关模型:

php
/**
 * 获取用户的最旧订单。
 */
public function oldestOrder(): HasOne
{
    return $this->hasOne(Order::class)->oldestOfMany();
}

默认情况下,latestOfManyoldestOfMany 方法将根据模型的主键检索最新或最旧的相关模型,该主键必须是可排序的。然而,有时您可能希望使用不同的排序标准从更大的关系中检索单个模型。

例如,使用 ofMany 方法,您可以检索用户的最贵订单。ofMany 方法接受可排序列作为第一个参数,并在查询相关模型时应用的聚合函数(minmax):

php
/**
 * 获取用户的最大订单。
 */
public function largestOrder(): HasOne
{
    return $this->hasOne(Order::class)->ofMany('price', 'max');
}
exclamation

由于 PostgreSQL 不支持对 UUID 列执行 MAX 函数,因此目前无法将多个关系与 PostgreSQL UUID 列结合使用。

将“多个”关系转换为“拥有一个”关系

通常,在使用 latestOfManyoldestOfManyofMany 方法检索单个模型时,您已经为同一模型定义了“拥有多个”关系。为了方便,Laravel 允许您通过在关系上调用 one 方法轻松将此关系转换为“拥有一个”关系:

php
/**
 * 获取用户的订单。
 */
public function orders(): HasMany
{
    return $this->hasMany(Order::class);
}

/**
 * 获取用户的最大订单。
 */
public function largestOrder(): HasOne
{
    return $this->orders()->one()->ofMany('price', 'max');
}

高级拥有多个中的一个关系

可以构建更高级的“拥有多个中的一个”关系。例如,一个 Product 模型可能有多个相关的 Price 模型,这些模型在系统中保留,即使发布了新的定价。此外,产品的新定价数据可能能够提前发布,以便在未来的某个日期生效,通过 published_at 列。

因此,总结一下,我们需要检索最新发布的定价,其中发布日期不在未来。此外,如果两个价格具有相同的发布日期,我们将优先选择 ID 最大的价格。为此,我们必须向 ofMany 方法传递一个数组,该数组包含确定最新价格的可排序列。此外,第二个参数将是一个闭包,负责向关系查询添加额外的发布日期约束:

php
/**
 * 获取产品的当前定价。
 */
public function currentPricing(): HasOne
{
    return $this->hasOne(Price::class)->ofMany([
        'published_at' => 'max',
        'id' => 'max',
    ], function (Builder $query) {
        $query->where('published_at', '<', now());
    });
}

通过一个拥有

“通过一个拥有”关系定义了与另一个模型的一对一关系。然而,这种关系表明声明模型可以通过第三个模型与另一个模型的一个实例匹配。

例如,在一个汽车修理店应用程序中,每个 Mechanic 模型可能与一个 Car 模型相关联,而每个 Car 模型可能与一个 Owner 模型相关联。虽然机械师和车主在数据库中没有直接关系,但机械师可以通过 Car 模型访问车主。让我们看看定义此关系所需的表:

php
mechanics
    id - integer
    name - string

cars
    id - integer
    model - string
    mechanic_id - integer

owners
    id - integer
    name - string
    car_id - integer

现在我们已经检查了关系所需的表结构,让我们在 Mechanic 模型上定义该关系:

php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasOneThrough;

class Mechanic extends Model
{
    /**
     * 获取汽车的车主。
     */
    public function carOwner(): HasOneThrough
    {
        return $this->hasOneThrough(Owner::class, Car::class);
    }
}

传递给 hasOneThrough 方法的第一个参数是我们希望访问的最终模型的名称,而第二个参数是中间模型的名称。

或者,如果相关关系已经在所有参与关系的模型上定义,您可以通过调用 through 方法并提供这些关系的名称来流畅地定义“通过一个拥有”关系。例如,如果 Mechanic 模型具有 cars 关系,而 Car 模型具有 owner 关系,您可以像这样定义连接机械师和车主的“通过一个拥有”关系:

php
// 基于字符串的语法...
return $this->through('cars')->has('owner');

// 动态语法...
return $this->throughCars()->hasOwner();

键约定

在执行关系查询时,将使用典型的 Eloquent 外键约定。如果您希望自定义关系的键,可以将它们作为第三和第四个参数传递给 hasOneThrough 方法。第三个参数是中间模型上的外键名称。第四个参数是最终模型上的外键名称。第五个参数是本地键,第六个参数是中间模型的本地键:

php
class Mechanic extends Model
{
    /**
     * 获取汽车的车主。
     */
    public function carOwner(): HasOneThrough
    {
        return $this->hasOneThrough(
            Owner::class,
            Car::class,
            'mechanic_id', // 汽车表上的外键...
            'car_id', // 车主表上的外键...
            'id', // 机械师表上的本地键...
            'id' // 汽车表上的本地键...
        );
    }
}

或者,如前所述,如果相关关系已经在所有参与关系的模型上定义,您可以通过调用 through 方法并提供这些关系的名称来流畅地定义“通过一个拥有”关系。这种方法的优点是重用已经在现有关系上定义的键约定:

php
// 基于字符串的语法...
return $this->through('cars')->has('owner');

// 动态语法...
return $this->throughCars()->hasOwner();

通过多个拥有

“通过多个拥有”关系提供了一种方便的方法,通过中间关系访问远程关系。例如,假设我们正在构建一个类似于 Laravel Vapor 的部署平台。一个 Project 模型可能通过中间 Environment 模型访问多个 Deployment 模型。使用此示例,您可以轻松收集给定项目的所有部署。让我们看看定义此关系所需的表:

php
projects
    id - integer
    name - string

environments
    id - integer
    project_id - integer
    name - string

deployments
    id - integer
    environment_id - integer
    commit_hash - string

现在我们已经检查了关系所需的表结构,让我们在 Project 模型上定义该关系:

php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasManyThrough;

class Project extends Model
{
    /**
     * 获取项目的所有部署。
     */
    public function deployments(): HasManyThrough
    {
        return $this->hasManyThrough(Deployment::class, Environment::class);
    }
}

传递给 hasManyThrough 方法的第一个参数是我们希望访问的最终模型的名称,而第二个参数是中间模型的名称。

或者,如果相关关系已经在所有参与关系的模型上定义,您可以通过调用 through 方法并提供这些关系的名称来流畅地定义“通过多个拥有”关系。例如,如果 Project 模型具有 environments 关系,而 Environment 模型具有 deployments 关系,您可以像这样定义连接项目和部署的“通过多个拥有”关系:

php
// 基于字符串的语法...
return $this->through('environments')->has('deployments');

// 动态语法...
return $this->throughEnvironments()->hasDeployments();

尽管 Deployment 模型的表中没有 project_id 列,但 hasManyThrough 关系通过 $project->deployments 提供对项目的部署的访问。要检索这些模型,Eloquent 会检查中间 Environment 模型表上的 project_id 列。在找到相关环境 ID 后,它们将用于查询 Deployment 模型的表。

键约定

在执行关系查询时,将使用典型的 Eloquent 外键约定。如果您希望自定义关系的键,可以将它们作为第三和第四个参数传递给 hasManyThrough 方法。第三个参数是中间模型上的外键名称。第四个参数是最终模型上的外键名称。第五个参数是本地键,第六个参数是中间模型的本地键:

php
class Project extends Model
{
    public function deployments(): HasManyThrough
    {
        return $this->hasManyThrough(
            Deployment::class,
            Environment::class,
            'project_id', // environments 表上的外键...
            'environment_id', // deployments 表上的外键...
            'id', // projects 表上的本地键...
            'id' // environments 表上的本地键...
        );
    }
}

或者,如前所述,如果相关关系已经在所有参与关系的模型上定义,您可以通过调用 through 方法并提供这些关系的名称来流畅地定义“通过多个拥有”关系。这种方法的优点是重用已经在现有关系上定义的键约定:

php
// 基于字符串的语法...
return $this->through('environments')->has('deployments');

// 动态语法...
return $this->throughEnvironments()->hasDeployments();

多对多关系

多对多关系比 hasOnehasMany 关系稍微复杂一些。多对多关系的一个示例是一个用户拥有多个角色,而这些角色也被应用程序中的其他用户共享。例如,一个用户可能被分配为“作者”和“编辑”;然而,这些角色也可能分配给其他用户。因此,一个用户有多个角色,而一个角色有多个用户。

表结构

要定义此关系,需要三个数据库表:usersrolesrole_userrole_user 表是从相关模型名称的字母顺序派生的,并包含 user_idrole_id 列。此表用作连接用户和角色的中间表。

请记住,由于角色可以属于多个用户,因此我们不能简单地在 roles 表上放置一个 user_id 列。这将意味着一个角色只能属于单个用户。为了支持将角色分配给多个用户,需要 role_user 表。我们可以总结关系的表结构如下:

php
users
    id - integer
    name - string

roles
    id - integer
    name - string

role_user
    user_id - integer
    role_id - integer

模型结构

多对多关系通过编写返回 belongsToMany 方法结果的方法来定义。belongsToMany 方法由所有应用程序的 Eloquent 模型使用的 Illuminate\Database\Eloquent\Model 基类提供。例如,让我们在 User 模型上定义一个 roles 方法。传递给此方法的第一个参数是相关模型类的名称:

php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;

class User extends Model
{
    /**
     * 属于用户的角色。
     */
    public function roles(): BelongsToMany
    {
        return $this->belongsToMany(Role::class);
    }
}

一旦关系被定义,您可以使用 roles 动态关系属性访问用户的角色:

php
use App\Models\User;

$user = User::find(1);

foreach ($user->roles as $role) {
    // ...
}

由于所有关系也充当查询构建器,您可以通过调用 roles 方法并继续链接条件到查询来向关系查询添加进一步的约束:

php
$roles = User::find(1)->roles()->orderBy('name')->get();

要确定关系的中间表的表名,Eloquent 将按字母顺序连接两个相关模型的名称。但是,您可以自由地覆盖此约定。您可以通过将第二个参数传递给 belongsToMany 方法来做到这一点:

php
return $this->belongsToMany(Role::class, 'role_user');

除了自定义中间表的名称外,您还可以通过将额外参数传递给 belongsToMany 方法自定义表上键的列名。第三个参数是您正在定义关系的模型的外键名称,而第四个参数是您要连接的模型的外键名称:

php
return $this->belongsToMany(Role::class, 'role_user', 'user_id', 'role_id');

定义关系的反向

要定义多对多关系的“反向”,您应该在相关模型上定义一个方法,该方法也返回 belongsToMany 方法的结果。为了完成我们的用户/角色示例,让我们在 Role 模型上定义 users 方法:

php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;

class Role extends Model
{
    /**
     * 属于角色的用户。
     */
    public function users(): BelongsToMany
    {
        return $this->belongsToMany(User::class);
    }
}

如您所见,关系的定义与其 User 模型对应部分完全相同,唯一的例外是引用 App\Models\User 模型。由于我们重用了 belongsToMany 方法,因此在定义多对多关系的“反向”时,所有通常的表和键自定义选项均可用。

检索中间表列

正如您已经了解到的,处理多对多关系需要中间表的存在。Eloquent 提供了一些非常有用的方法来与此表进行交互。例如,假设我们的 User 模型与多个 Role 模型相关。访问此关系后,我们可以使用 pivot 属性访问中间表:

php
use App\Models\User;

$user = User::find(1);

foreach ($user->roles as $role) {
    echo $role->pivot->created_at;
}

请注意,我们检索到的每个 Role 模型都自动分配了一个 pivot 属性。此属性包含一个表示中间表的模型。

默认情况下,只有模型键将存在于 pivot 模型上。如果您的中间表包含额外的属性,则必须在定义关系时指定它们:

php
return $this->belongsToMany(Role::class)->withPivot('active', 'created_by');

如果您希望中间表具有由 Eloquent 自动维护的 created_atupdated_at 时间戳,请在定义关系时调用 withTimestamps 方法:

php
return $this->belongsToMany(Role::class)->withTimestamps();
exclamation

使用 Eloquent 自动维护时间戳的中间表必须具有 created_atupdated_at 时间戳列。

自定义 pivot 属性名称

如前所述,可以通过 pivot 属性在模型上访问中间表的属性。但是,您可以自由地自定义此属性的名称,以更好地反映其在应用程序中的用途。

例如,如果您的应用程序包含可能订阅播客的用户,则可能存在用户与播客之间的多对多关系。如果是这种情况,您可能希望将中间表属性重命名为 subscription 而不是 pivot。这可以通过在定义关系时使用 as 方法来完成:

php
return $this->belongsToMany(Podcast::class)
                ->as('subscription')
                ->withTimestamps();

一旦指定了自定义中间表属性,您可以使用自定义名称访问中间表数据:

php
$users = User::with('podcasts')->get();

foreach ($users->flatMap->podcasts as $podcast) {
    echo $podcast->subscription->created_at;
}

通过中间表列过滤查询

您还可以使用 wherePivotwherePivotInwherePivotNotInwherePivotBetweenwherePivotNotBetweenwherePivotNullwherePivotNotNull 方法在定义关系时使用中间表列过滤返回的结果:

php
return $this->belongsToMany(Role::class)
                ->wherePivot('approved', 1);

return $this->belongsToMany(Role::class)
                ->wherePivotIn('priority', [1, 2]);

return $this->belongsToMany(Role::class)
                ->wherePivotNotIn('priority', [1, 2]);

return $this->belongsToMany(Podcast::class)
                ->as('subscriptions')
                ->wherePivotBetween('created_at', ['2020-01-01 00:00:00', '2020-12-31 00:00:00']);

return $this->belongsToMany(Podcast::class)
                ->as('subscriptions')
                ->wherePivotNotBetween('created_at', ['2020-01-01 00:00:00', '2020-12-31 00:00:00']);

return $this->belongsToMany(Podcast::class)
                ->as('subscriptions')
                ->wherePivotNull('expired_at');

return $this->belongsToMany(Podcast::class)
                ->as('subscriptions')
                ->wherePivotNotNull('expired_at');

通过中间表列排序查询

您可以使用 orderByPivot 方法对 belongsToMany 关系查询返回的结果进行排序。在以下示例中,我们将检索用户的所有最新徽章:

php
return $this->belongsToMany(Badge::class)
                ->where('rank', 'gold')
                ->orderByPivot('created_at', 'desc');

定义自定义中间表模型

如果您希望为多对多关系定义一个自定义模型来表示中间表,您可以在定义关系时调用 using 方法。自定义中间模型使您有机会在中间模型上定义额外的行为,例如方法和类型转换。

自定义多对多中间模型应扩展 Illuminate\Database\Eloquent\Relations\Pivot 类,而自定义多态多对多中间模型应扩展 Illuminate\Database\Eloquent\Relations\MorphPivot 类。例如,我们可以定义一个使用自定义 RoleUser 中间模型的 Role 模型:

php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;

class Role extends Model
{
    /**
     * 属于角色的用户。
     */
    public function users(): BelongsToMany
    {
        return $this->belongsToMany(User::class)->using(RoleUser::class);
    }
}

在定义 RoleUser 模型时,您应该扩展 Illuminate\Database\Eloquent\Relations\Pivot 类:

php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Relations\Pivot;

class RoleUser extends Pivot
{
    // ...
}
exclamation

中间模型可能无法使用 SoftDeletes 特性。如果您需要软删除中间记录,请考虑将中间模型转换为实际的 Eloquent 模型。

自定义中间模型和自增 ID

如果您定义了一个使用自定义中间模型的多对多关系,并且该中间模型具有自增主键,则应确保您的自定义中间模型类定义了一个 incrementing 属性,该属性设置为 true

php
/**
 * 指示 ID 是否为自增。
 *
 * @var bool
 */
public $incrementing = true;

多态关系

多态关系允许子模型使用单个关联属于多个类型的模型。例如,假设您正在构建一个允许用户共享博客文章和视频的应用程序。在这样的应用程序中,Comment 模型可能同时属于 PostVideo 模型。

一对一(多态)

表结构

一对一多态关系类似于典型的一对一关系;然而,子模型可以使用单个关联属于多个类型的模型。例如,博客 PostUser 可能共享与 Image 模型的多态关系。使用一对一多态关系,您可以拥有一个唯一图像的单个表,该图像可以与帖子和用户相关联。首先,让我们检查表结构:

php
posts
    id - integer
    name - string

users
    id - integer
    name - string

images
    id - integer
    url - string
    imageable_id - integer
    imageable_type - string

请注意 images 表上的 imageable_idimageable_type 列。imageable_id 列将包含帖子或用户的 ID 值,而 imageable_type 列将包含父模型的类名。Eloquent 使用 imageable_type 列来确定在访问 imageable 关系时返回哪个“类型”的父模型。在这种情况下,该列将包含 App\Models\PostApp\Models\User

模型结构

接下来,让我们检查构建此关系所需的模型定义:

php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\MorphTo;

class Image extends Model
{
    /**
     * 获取父图像模型(用户或帖子)。
     */
    public function imageable(): MorphTo
    {
        return $this->morphTo();
    }
}

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\MorphOne;

class Post extends Model
{
    /**
     * 获取帖子的图像。
     */
    public function image(): MorphOne
    {
        return $this->morphOne(Image::class, 'imageable');
    }
}

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\MorphOne;

class User extends Model
{
    /**
     * 获取用户的图像。
     */
    public function image(): MorphOne
    {
        return $this->morphOne(Image::class, 'imageable');
    }
}

检索关系

一旦您的数据库表和模型定义完成,您可以通过模型访问关系。例如,要检索帖子的图像,我们可以访问 image 动态关系属性:

php
use App\Models\Post;

$post = Post::find(1);

$image = $post->image;

您可以通过访问执行调用 morphTo 的方法的名称来检索多态模型的父模型。在这种情况下,即 Image 模型上的 imageable 方法。因此,我们将该方法作为动态关系属性访问,以访问图像的父模型:

php
use App\Models\Image;

$image = Image::find(1);

$imageable = $image->imageable;

Image 模型上的 imageable 关系将返回 PostUser 实例,具体取决于哪个类型的模型拥有该图像。

键约定

如果需要,您可以指定多态子模型使用的“id”和“type”列的名称。如果这样做,请确保始终将关系名称作为第一个参数传递给 morphTo 方法。通常,此值应与方法名称匹配,因此您可以使用 PHP 的 __FUNCTION__ 常量:

php
/**
 * 获取图像所属的模型。
 */
public function imageable(): MorphTo
{
    return $this->morphTo(__FUNCTION__, 'imageable_type', 'imageable_id');
}

一对多(多态)

表结构

一对多多态关系类似于典型的一对多关系;然而,子模型可以使用单个关联属于多个类型的模型。例如,想象一下,您应用程序的用户可以对帖子和视频进行“评论”。使用多态关系,您可以使用单个 comments 表来包含对帖子和视频的评论。首先,让我们检查构建此关系所需的表结构:

php
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

模型结构

接下来,让我们检查构建此关系所需的模型定义:

php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\MorphTo;

class Comment extends Model
{
    /**
     * 获取父可评论模型(帖子或视频)。
     */
    public function commentable(): MorphTo
    {
        return $this->morphTo();
    }
}

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\MorphMany;

class Post extends Model
{
    /**
     * 获取所有帖子的评论。
     */
    public function comments(): MorphMany
    {
        return $this->morphMany(Comment::class, 'commentable');
    }
}

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\MorphMany;

class Video extends Model
{
    /**
     * 获取所有视频的评论。
     */
    public function comments(): MorphMany
    {
        return $this->morphMany(Comment::class, 'commentable');
    }
}

检索关系

一旦您的数据库表和模型定义完成,您可以通过模型的动态关系属性访问关系。例如,要访问帖子的所有评论,我们可以使用 comments 动态属性:

php
use App\Models\Post;

$post = Post::find(1);

foreach ($post->comments as $comment) {
    // ...
}

您还可以通过访问执行调用 morphTo 的方法的名称来检索多态子模型的父模型。在这种情况下,即 Comment 模型上的 commentable 方法。因此,我们将该方法作为动态关系属性访问,以访问评论的父模型:

php
use App\Models\Comment;

$comment = Comment::find(1);

$commentable = $comment->commentable;

Comment 模型上的 commentable 关系将返回 PostVideo 实例,具体取决于哪个类型的模型是评论的父模型。

自动为子模型填充父模型

即使在使用 Eloquent 急切加载时,如果您尝试在循环遍历子模型时访问父模型,也可能会出现“N + 1”查询问题:

php
$posts = Post::with('comments')->get();

foreach ($posts as $post) {
    foreach ($post->comments as $comment) {
        echo $comment->commentable->title;
    }
}

在上面的示例中,由于即使为每个 Post 模型急切加载了评论,Eloquent 也不会自动为每个子 Comment 模型填充父 Post

如果您希望 Eloquent 自动将父模型填充到其子模型中,您可以在定义 morphMany 关系时调用 chaperone 方法:

php
class Post extends Model
{
    /**
     * 获取所有帖子的评论。
     */
    public function comments(): MorphMany
    {
        return $this->morphMany(Comment::class, 'commentable')->chaperone();
    }
}

或者,如果您希望在运行时选择性地启用自动父填充,您可以在急切加载关系时调用 chaperone 模型:

php
use App\Models\Post;

$posts = Post::with([
    'comments' => fn ($comments) => $comments->chaperone(),
])->get();

多个中的一个(多态)

有时,一个模型可能有多个相关模型,但您希望方便地检索用户上传的“最新”或“最旧”相关模型。例如,一个 User 模型可能与多个 Image 模型相关,但您希望定义一种方便的方式来与用户上传的最新图像进行交互。您可以使用 morphOne 关系类型结合 ofMany 方法来实现:

php
/**
 * 获取用户的最新图像。
 */
public function latestImage(): MorphOne
{
    return $this->morphOne(Image::class, 'imageable')->latestOfMany();
}

同样,您可以定义一个方法来检索“最旧”或第一个相关模型:

php
/**
 * 获取用户的最旧图像。
 */
public function oldestImage(): MorphOne
{
    return $this->morphOne(Image::class, 'imageable')->oldestOfMany();
}

默认情况下,latestOfManyoldestOfMany 方法将根据模型的主键检索最新或最旧的相关模型,该主键必须是可排序的。然而,有时您可能希望使用不同的排序标准从更大的关系中检索单个模型。

例如,使用 ofMany 方法,您可以检索用户的最“受欢迎”的图像。ofMany 方法接受可排序列作为第一个参数,并在查询相关模型时应用的聚合函数(minmax):

php
/**
 * 获取用户的最受欢迎的图像。
 */
public function bestImage(): MorphOne
{
    return $this->morphOne(Image::class, 'imageable')->ofMany('likes', 'max');
}
lightbulb

可以构建更高级的“多个中的一个”关系。有关更多信息,请查阅 拥有多个中的一个文档

多对多(多态)

表结构

多对多多态关系比“morph one”和“morph many”关系稍微复杂一些。例如,一个 Post 模型和 Video 模型可以共享与 Tag 模型的多态关系。在这种情况下,使用多对多多态关系将允许您的应用程序拥有一个唯一标签的单个表,该标签可以与帖子或视频相关联。首先,让我们检查构建此关系所需的表结构:

php
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
lightbulb

在深入了解多态多对多关系之前,您可能会受益于阅读有关典型 多对多关系 的文档。

模型结构

接下来,我们准备在模型上定义关系。PostVideo 模型都将包含一个调用 morphToMany 方法的 tags 方法。

morphToMany 方法接受相关模型的名称以及“关系名称”。根据我们分配给中间表名称及其包含的键的名称,我们将该关系称为“可标记”:

php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\MorphToMany;

class Post extends Model
{
    /**
     * 获取帖子的所有标签。
     */
    public function tags(): MorphToMany
    {
        return $this->morphToMany(Tag::class, 'taggable');
    }
}

定义关系的反向

接下来,在 Tag 模型上,您应该为其可能的每个父模型定义一个方法。因此,在此示例中,我们将定义 posts 方法和 videos 方法。这两个方法都应返回 morphedByMany 方法的结果。

morphedByMany 方法接受相关模型的名称以及“关系名称”。根据我们分配给中间表名称及其包含的键的名称,我们将该关系称为“可标记”:

php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\MorphToMany;

class Tag extends Model
{
    /**
     * 获取分配给此标签的所有帖子。
     */
    public function posts(): MorphToMany
    {
        return $this->morphedByMany(Post::class, 'taggable');
    }

    /**
     * 获取分配给此标签的所有视频。
     */
    public function videos(): MorphToMany
    {
        return $this->morphedByMany(Video::class, 'taggable');
    }
}

检索关系

一旦您的数据库表和模型定义完成,您可以通过模型访问关系。例如,要访问帖子的所有标签,您可以使用 tags 动态关系属性:

php
use App\Models\Post;

$post = Post::find(1);

foreach ($post->tags as $tag) {
    // ...
}

您可以通过访问执行调用 morphedByMany 的方法的名称来从多态子模型检索父模型。在这种情况下,即 Tag 模型上的 postsvideos 方法:

php
use App\Models\Tag;

$tag = Tag::find(1);

foreach ($tag->posts as $post) {
    // ...
}

foreach ($tag->videos as $video) {
    // ...
}

自定义多态类型

默认情况下,Laravel 将使用完全限定的类名来存储相关模型的“类型”。例如,考虑上面的“一对多关系”示例,其中 Comment 模型可能属于 PostVideo 模型,默认的 commentable_type 将是 App\Models\PostApp\Models\Video。但是,您可能希望将这些值与应用程序的内部结构解耦。

例如,而不是使用模型名称作为“类型”,我们可以使用简单的字符串,例如 postvideo。通过这样做,多态“类型”列的值将保持有效,即使模型被重命名:

php
use Illuminate\Database\Eloquent\Relations\Relation;

Relation::enforceMorphMap([
    'post' => 'App\Models\Post',
    'video' => 'App\Models\Video',
]);

您可以在 App\Providers\AppServiceProvider 类的 boot 方法中调用 enforceMorphMap 方法,或者如果您愿意,可以创建一个单独的服务提供者。

您可以在运行时使用模型的 getMorphClass 方法确定给定模型的多态别名。相反,您可以使用 Relation::getMorphedModel 方法确定与多态别名相关联的完全限定类名:

php
use Illuminate\Database\Eloquent\Relations\Relation;

$alias = $post->getMorphClass();

$class = Relation::getMorphedModel($alias);
exclamation

在现有应用程序中添加“形态映射”时,数据库中仍包含完全限定类的每个可多态 *_type 列值都需要转换为其“映射”名称。

动态关系

您可以使用 resolveRelationUsing 方法在运行时定义 Eloquent 模型之间的关系。虽然通常不建议在正常应用程序开发中使用,但在开发 Laravel 包时,这可能偶尔会很有用。

resolveRelationUsing 方法接受所需关系名称作为第一个参数。传递给方法的第二个参数应该是一个闭包,该闭包接受模型实例并返回有效的 Eloquent 关系定义。通常,您应该在 服务提供者boot 方法中配置动态关系:

php
use App\Models\Order;
use App\Models\Customer;

Order::resolveRelationUsing('customer', function (Order $orderModel) {
    return $orderModel->belongsTo(Customer::class, 'customer_id');
});
exclamation

在定义动态关系时,始终为 Eloquent 关系方法提供显式键名参数。

查询关系

由于所有 Eloquent 关系都是通过方法定义的,因此您可以调用这些方法以获取关系的实例,而无需实际执行查询以加载相关模型。此外,所有类型的 Eloquent 关系也充当 查询构建器,允许您在最终执行 SQL 查询之前继续链接约束到关系查询。

例如,想象一下一个博客应用程序,其中 User 模型与多个相关的 Post 模型:

php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;

class User extends Model
{
    /**
     * 获取用户的所有帖子。
     */
    public function posts(): HasMany
    {
        return $this->hasMany(Post::class);
    }
}

您可以查询 posts 关系并添加额外的约束,如下所示:

php
use App\Models\User;

$user = User::find(1);

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

您可以在关系上使用 Laravel 查询构建器 的任何方法,因此请务必查看查询构建器文档,以了解可用的所有方法。

在关系后链接 orWhere 子句

正如上面的示例所示,您可以在查询关系时向其添加额外的约束。但是,在关系上链接 orWhere 子句时要小心,因为 orWhere 子句将在与关系约束相同的级别上进行逻辑分组:

php
$user->posts()
        ->where('active', 1)
        ->orWhere('votes', '>=', 100)
        ->get();

上面的示例将生成以下 SQL。可以看到,or 子句指示查询返回 任何 投票数大于 100 的帖子。查询不再限制于特定用户:

sql
select *
from posts
where user_id = ? and active = 1 or votes >= 100

在大多数情况下,您应该使用 逻辑分组 将条件检查分组在括号之间:

php
use Illuminate\Database\Eloquent\Builder;

$user->posts()
        ->where(function (Builder $query) {
            return $query->where('active', 1)
                         ->orWhere('votes', '>=', 100);
        })
        ->get();

上面的示例将生成以下 SQL。请注意,逻辑分组已正确分组约束,查询仍然限制于特定用户:

sql
select *
from posts
where user_id = ? and (active = 1 or votes >= 100)

关系方法与动态属性

如果您不需要向 Eloquent 关系查询添加额外的约束,则可以像访问属性一样访问关系。例如,继续使用我们的 UserPost 示例模型,我们可以像这样访问用户的所有帖子:

php
use App\Models\User;

$user = User::find(1);

foreach ($user->posts as $post) {
    // ...
}

动态关系属性执行“懒加载”,这意味着它们仅在您实际访问它们时才会加载其关系数据。因此,开发人员通常使用 急切加载 来预加载他们知道在加载模型后将被访问的关系。急切加载显著减少了必须执行的 SQL 查询,以加载模型的关系。

查询关系存在性

在检索模型记录时,您可能希望根据关系的存在性限制结果。例如,想象一下,您想检索所有至少有一条评论的博客帖子。为此,您可以将关系的名称传递给 hasorHas 方法:

php
use App\Models\Post;

// 检索所有至少有一条评论的帖子...
$posts = Post::has('comments')->get();

您还可以指定运算符和计数值,以进一步自定义查询:

php
// 检索所有至少有三条评论的帖子...
$posts = Post::has('comments', '>=', 3)->get();

可以使用“点”符号构建嵌套的 has 语句。例如,您可以检索所有至少有一条评论且至少有一张图片的帖子:

php
// 检索至少有一条评论且有图片的帖子...
$posts = Post::has('comments.images')->get();

如果您需要更多功能,您可以使用 whereHasorWhereHas 方法在 has 查询上定义额外的查询约束,例如检查评论的内容:

php
use Illuminate\Database\Eloquent\Builder;

// 检索包含类似 code% 的单词的评论的帖子...
$posts = Post::whereHas('comments', function (Builder $query) {
    $query->where('content', 'like', 'code%');
})->get();

// 检索包含类似 code% 的单词的至少十条评论的帖子...
$posts = Post::whereHas('comments', function (Builder $query) {
    $query->where('content', 'like', 'code%');
}, '>=', 10)->get();
exclamation

Eloquent 目前不支持跨数据库查询关系的存在性。关系必须存在于同一数据库中。

内联关系存在性查询

如果您希望使用单个简单的条件附加到关系查询中查询关系的存在性,您可能会发现使用 whereRelationorWhereRelationwhereMorphRelationorWhereMorphRelation 方法更方便。例如,我们可以查询所有具有未批准评论的帖子:

php
use App\Models\Post;

$posts = Post::whereRelation('comments', 'is_approved', false)->get();

当然,像查询构建器的 where 方法一样,您还可以指定运算符:

php
$posts = Post::whereRelation(
    'comments', 'created_at', '>=', now()->subHour()
)->get();

查询关系缺失

在检索模型记录时,您可能希望根据关系的缺失限制结果。例如,想象一下,您想检索所有没有评论的博客帖子。为此,您可以将关系的名称传递给 doesntHaveorDoesntHave 方法:

php
use App\Models\Post;

$posts = Post::doesntHave('comments')->get();

如果您需要更多功能,您可以使用 whereDoesntHaveorWhereDoesntHave 方法在 doesntHave 查询上添加额外的查询约束,例如检查评论的内容:

php
use Illuminate\Database\Eloquent\Builder;

$posts = Post::whereDoesntHave('comments', function (Builder $query) {
    $query->where('content', 'like', 'code%');
})->get();

您可以使用“点”符号对嵌套关系执行查询。例如,以下查询将检索所有没有评论的帖子;但是,包含来自未被禁止的作者的评论的帖子将包含在结果中:

php
use Illuminate\Database\Eloquent\Builder;

$posts = Post::whereDoesntHave('comments.author', function (Builder $query) {
    $query->where('banned', 0);
})->get();

查询 Morph To 关系

要查询“morph to”关系的存在性,您可以使用 whereHasMorphwhereDoesntHaveMorph 方法。这些方法将关系的名称作为第一个参数。接下来,这些方法接受您希望在查询中包含的相关模型的名称。最后,您可以提供一个闭包来定制关系查询:

php
use App\Models\Comment;
use App\Models\Post;
use App\Models\Video;
use Illuminate\Database\Eloquent\Builder;

// 检索与帖子或视频相关的评论,标题类似 code%...
$comments = Comment::whereHasMorph(
    'commentable',
    [Post::class, Video::class],
    function (Builder $query) {
        $query->where('title', 'like', 'code%');
    }
)->get();

// 检索与帖子相关的评论,标题不类似 code%...
$comments = Comment::whereDoesntHaveMorph(
    'commentable',
    Post::class,
    function (Builder $query) {
        $query->where('title', 'like', 'code%');
    }
)->get();

您可能偶尔需要根据相关多态模型的“类型”添加查询约束。传递给 whereHasMorph 方法的闭包可以接收作为第二个参数的 $type 值。此参数允许您检查正在构建的查询的“类型”:

php
use Illuminate\Database\Eloquent\Builder;

$comments = Comment::whereHasMorph(
    'commentable',
    [Post::class, Video::class],
    function (Builder $query, string $type) {
        $column = $type === Post::class ? 'content' : 'title';

        $query->where($column, 'like', 'code%');
    }
)->get();

有时,您可能希望查询“morph to”关系的父项的子项。您可以使用 whereMorphedTowhereNotMorphedTo 方法来实现,这将自动确定给定模型的正确多态类型映射。这些方法接受多态 morphTo 关系的名称作为第一个参数,以及相关父模型作为第二个参数:

php
$comments = Comment::whereMorphedTo('commentable', $post)
                      ->orWhereMorphedTo('commentable', $video)
                      ->get();

查询所有相关模型

您可以通过将 * 作为通配符值提供,而不是传递可能的多态模型数组。这将指示 Laravel 从数据库中检索所有可能的多态类型。Laravel 将执行额外的查询以执行此操作:

php
use Illuminate\Database\Eloquent\Builder;

$comments = Comment::whereHasMorph('commentable', '*', function (Builder $query) {
    $query->where('title', 'like', 'foo%');
})->get();

聚合相关模型

计数相关模型

有时,您可能希望在不实际加载模型的情况下计算给定关系的相关模型数量。为此,您可以使用 withCount 方法。withCount 方法将在结果模型上放置一个 {relation}_count 属性:

php
use App\Models\Post;

$posts = Post::withCount('comments')->get();

foreach ($posts as $post) {
    echo $post->comments_count;
}

通过将数组传递给 withCount 方法,您可以为多个关系添加“计数”,并为查询添加额外的约束:

php
use Illuminate\Database\Eloquent\Builder;

$posts = Post::withCount(['votes', 'comments' => function (Builder $query) {
    $query->where('content', 'like', 'code%');
}])->get();

echo $posts[0]->votes_count;
echo $posts[0]->comments_count;

您还可以为关系计数结果指定别名,从而在同一关系上进行多个计数:

php
use Illuminate\Database\Eloquent\Builder;

$posts = Post::withCount([
    'comments',
    'comments as pending_comments_count' => function (Builder $query) {
        $query->where('approved', false);
    },
])->get();

echo $posts[0]->comments_count;
echo $posts[0]->pending_comments_count;

延迟计数加载

使用 loadCount 方法,您可以在父模型已经被检索后加载关系计数:

php
$book = Book::first();

$book->loadCount('genres');

如果您需要在计数查询上设置额外的查询约束,您可以传递一个以您希望计数的关系为键的数组。数组值应是接收查询构建器实例的闭包:

php
$book->loadCount(['reviews' => function (Builder $query) {
    $query->where('rating', 5);
}])

关系计数和自定义选择语句

如果您将 withCountselect 语句结合使用,请确保在调用 select 方法后调用 withCount

php
$posts = Post::select(['title', 'body'])
                ->withCount('comments')
                ->get();

其他聚合函数

除了 withCount 方法,Eloquent 还提供 withMinwithMaxwithAvgwithSumwithExists 方法。这些方法将在结果模型上放置一个 {relation}_{function}_{column} 属性:

php
use App\Models\Post;

$posts = Post::withSum('comments', 'votes')->get();

foreach ($posts as $post) {
    echo $post->comments_sum_votes;
}

如果您希望使用另一个名称访问聚合函数的结果,您可以指定自己的别名:

php
$posts = Post::withSum('comments as total_comments', 'votes')->get();

foreach ($posts as $post) {
    echo $post->total_comments;
}

loadCount 方法一样,这些方法的延迟版本也可用。这些额外的聚合操作可以在已经检索到的 Eloquent 模型上执行:

php
$post = Post::first();

$post->loadSum('comments', 'votes');

如果您将这些聚合方法与 select 语句结合使用,请确保在调用 select 方法后调用聚合方法:

php
$posts = Post::select(['title', 'body'])
                ->withExists('comments')
                ->get();

计数 Morph To 关系上的相关模型

如果您希望急切加载“morph to”关系,以及与可能返回的各种实体相关的模型计数,您可以使用 with 方法结合 morphTo 关系的 morphWithCount 方法。

在此示例中,假设 PhotoPost 模型可能创建 ActivityFeed 模型。我们将假设 ActivityFeed 模型定义了一个名为 parentable 的“morph to”关系,允许我们检索给定 ActivityFeed 实例的父 PhotoPost 模型。此外,假设 Photo 模型“拥有多个” Tag 模型,而 Post 模型“拥有多个” Comment 模型。

现在,让我们想象我们想要检索 ActivityFeed 实例,并急切加载每个 ActivityFeed 实例的 parentable 父模型。此外,我们希望检索与每个父照片相关的标签数量和与每个父帖子相关的评论数量:

php
use Illuminate\Database\Eloquent\Relations\MorphTo;

$activities = ActivityFeed::with([
    'parentable' => function (MorphTo $morphTo) {
        $morphTo->morphWithCount([
            Photo::class => ['tags'],
            Post::class => ['comments'],
        ]);
    }])->get();

延迟计数加载

假设我们已经检索到一组 ActivityFeed 模型,现在我们希望加载与各种 parentable 模型相关的嵌套关系计数。您可以使用 loadMorphCount 方法来实现:

php
$activities = ActivityFeed::with('parentable')->get();

$activities->loadMorphCount('parentable', [
    Photo::class => ['tags'],
    Post::class => ['comments'],
]);

急切加载

当将 Eloquent 关系作为属性访问时,相关模型是“懒加载”的。这意味着关系数据在您第一次访问属性时不会实际加载。但是,Eloquent 可以在查询父模型时“急切加载”关系。急切加载缓解了“N + 1”查询问题。为了说明 N + 1 查询问题,考虑一个“书籍”模型,它“属于”一个“作者”模型:

php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;

class Book extends Model
{
    /**
     * 获取撰写书籍的作者。
     */
    public function author(): BelongsTo
    {
        return $this->belongsTo(Author::class);
    }
}

现在,让我们检索所有书籍及其作者:

php
use App\Models\Book;

$books = Book::all();

foreach ($books as $book) {
    echo $book->author->name;
}

此循环将执行一个查询以检索数据库表中的所有书籍,然后为每本书执行另一个查询以检索书籍的作者。因此,如果我们有 25 本书,上面的代码将运行 26 个查询:一个用于原始书籍,25 个额外查询以检索每本书的作者。

幸运的是,我们可以使用急切加载将此操作减少到仅两个查询。当构建查询时,您可以使用 with 方法指定应急切加载哪些关系:

php
$books = Book::with('author')->get();

foreach ($books as $book) {
    echo $book->author->name;
}

对于此操作,仅会执行两个查询 - 一个查询以检索所有书籍,一个查询以检索所有书籍的所有作者:

sql
select * from books

select * from authors where id in (1, 2, 3, 4, 5, ...)

急切加载多个关系

有时,您可能需要急切加载多个不同的关系。为此,只需将关系数组传递给 with 方法:

php
$books = Book::with(['author', 'publisher'])->get();

嵌套预加载

要预加载关系的关系,您可以使用“点”语法。例如,让我们预加载所有书籍的作者和所有作者的个人联系人:

php
$books = Book::with('author.contacts')->get();

或者,您可以通过向 with 方法提供嵌套数组来指定嵌套的预加载关系,这在预加载多个嵌套关系时非常方便:

php
$books = Book::with([
    'author' => [
        'contacts',
        'publisher',
    ],
])->get();

嵌套预加载 morphTo 关系

如果您想要预加载 morphTo 关系,以及可能由该关系返回的各种实体的嵌套关系,您可以使用 with 方法与 morphTo 关系的 morphWith 方法结合使用。为了帮助说明这种方法,让我们考虑以下模型:

php
<?php

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\MorphTo;

class ActivityFeed extends Model
{
    /**
     * 获取活动记录的父级。
     */
    public function parentable(): MorphTo
    {
        return $this->morphTo();
    }
}

在这个例子中,假设 EventPhotoPost 模型可能创建 ActivityFeed 模型。此外,假设 Event 模型属于 Calendar 模型,Photo 模型与 Tag 模型相关联,Post 模型属于 Author 模型。

使用这些模型定义和关系,我们可以检索 ActivityFeed 模型实例并预加载所有 parentable 模型及其各自的嵌套关系:

php
use Illuminate\Database\Eloquent\Relations\MorphTo;

$activities = ActivityFeed::query()
    ->with(['parentable' => function (MorphTo $morphTo) {
        $morphTo->morphWith([
            Event::class => ['calendar'],
            Photo::class => ['tags'],
            Post::class => ['author'],
        ]);
    }])->get();

预加载特定列

您可能并不总是需要从所检索的关系中获取每一列。因此,Eloquent 允许您指定希望检索的关系的列:

php
$books = Book::with('author:id,name,book_id')->get();
exclamation

使用此功能时,您应始终在希望检索的列列表中包含 id 列和任何相关的外键列。

默认预加载

有时您可能希望在检索模型时始终加载某些关系。为此,您可以在模型上定义 $with 属性:

php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;

class Book extends Model
{
    /**
     * 应始终加载的关系。
     *
     * @var array
     */
    protected $with = ['author'];

    /**
     * 获取撰写书籍的作者。
     */
    public function author(): BelongsTo
    {
        return $this->belongsTo(Author::class);
    }

    /**
     * 获取书籍的类型。
     */
    public function genre(): BelongsTo
    {
        return $this->belongsTo(Genre::class);
    }
}

如果您希望在单个查询中从 $with 属性中移除某个项,可以使用 without 方法:

php
$books = Book::without('author')->get();

如果您希望在单个查询中覆盖 $with 属性中的所有项,可以使用 withOnly 方法:

php
$books = Book::withOnly('genre')->get();

限制预加载

有时您可能希望预加载一个关系,但同时为预加载查询指定额外的查询条件。您可以通过将关系数组传递给 with 方法来实现,其中数组键是关系名称,数组值是添加额外约束的闭包:

php
use App\Models\User;
use Illuminate\Contracts\Database\Eloquent\Builder;

$users = User::with(['posts' => function (Builder $query) {
    $query->where('title', 'like', '%code%');
}])->get();

在这个例子中,Eloquent 只会预加载 title 列包含单词 code 的帖子。您可以调用其他 查询构建器 方法进一步自定义预加载操作:

php
$users = User::with(['posts' => function (Builder $query) {
    $query->orderBy('created_at', 'desc');
}])->get();

限制 morphTo 关系的预加载

如果您正在预加载 morphTo 关系,Eloquent 将运行多个查询以获取每种类型的相关模型。您可以使用 MorphTo 关系的 constrain 方法为每个查询添加额外的约束:

php
use Illuminate\Database\Eloquent\Relations\MorphTo;

$comments = Comment::with(['commentable' => function (MorphTo $morphTo) {
    $morphTo->constrain([
        Post::class => function ($query) {
            $query->whereNull('hidden_at');
        },
        Video::class => function ($query) {
            $query->where('type', 'educational');
        },
    ]);
}])->get();

在这个例子中,Eloquent 只会预加载未被隐藏的帖子和类型值为“educational”的视频。

使用关系存在限制预加载

您有时可能需要检查关系的存在,同时根据相同条件加载该关系。例如,您可能希望仅检索具有匹配给定查询条件的子 Post 模型的 User 模型,同时预加载匹配的帖子。您可以使用 withWhereHas 方法来实现:

php
use App\Models\User;

$users = User::withWhereHas('posts', function ($query) {
    $query->where('featured', true);
})->get();

懒惰预加载

有时您可能需要在父模型已经被检索后预加载一个关系。例如,如果您需要动态决定是否加载相关模型,这可能会很有用:

php
use App\Models\Book;

$books = Book::all();

if ($someCondition) {
    $books->load('author', 'publisher');
}

如果您需要在预加载查询上设置额外的查询约束,您可以传递一个以您希望加载的关系为键的数组。数组值应为接收查询实例的闭包:

php
$author->load(['books' => function (Builder $query) {
    $query->orderBy('published_date', 'asc');
}]);

要仅在尚未加载时加载关系,请使用 loadMissing 方法:

php
$book->loadMissing('author');

嵌套懒惰预加载和 morphTo

如果您想要预加载 morphTo 关系,以及可能由该关系返回的各种实体的嵌套关系,您可以使用 loadMorph 方法。

该方法接受 morphTo 关系的名称作为第一个参数,以及模型/关系对的数组作为第二个参数。为了帮助说明这种方法,让我们考虑以下模型:

php
<?php

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\MorphTo;

class ActivityFeed extends Model
{
    /**
     * 获取活动记录的父级。
     */
    public function parentable(): MorphTo
    {
        return $this->morphTo();
    }
}

在这个例子中,假设 EventPhotoPost 模型可能创建 ActivityFeed 模型。此外,假设 Event 模型属于 Calendar 模型,Photo 模型与 Tag 模型相关联,Post 模型属于 Author 模型。

使用这些模型定义和关系,我们可以检索 ActivityFeed 模型实例并预加载所有 parentable 模型及其各自的嵌套关系:

php
$activities = ActivityFeed::with('parentable')
    ->get()
    ->loadMorph('parentable', [
        Event::class => ['calendar'],
        Photo::class => ['tags'],
        Post::class => ['author'],
    ]);

防止懒惰加载

如前所述,预加载关系通常可以为您的应用程序提供显著的性能优势。因此,如果您愿意,您可以指示 Laravel 始终防止懒惰加载关系。为此,您可以在应用程序的 AppServiceProvider 类的 boot 方法中调用 preventLazyLoading 方法。

preventLazyLoading 方法接受一个可选的布尔参数,指示是否应防止懒惰加载。例如,您可能希望仅在非生产环境中禁用懒惰加载,以便您的生产环境在生产代码中意外存在懒惰加载关系时仍能正常运行:

php
use Illuminate\Database\Eloquent\Model;

/**
 * 启动任何应用程序服务。
 */
public function boot(): void
{
    Model::preventLazyLoading(! $this->app->isProduction());
}

在防止懒惰加载后,当您的应用程序尝试懒惰加载任何 Eloquent 关系时,Eloquent 将抛出 Illuminate\Database\LazyLoadingViolationException 异常。

您可以使用 handleLazyLoadingViolationsUsing 方法自定义懒惰加载违规的行为。例如,使用此方法,您可以指示懒惰加载违规仅记录,而不是通过异常中断应用程序的执行:

php
Model::handleLazyLoadingViolationUsing(function (Model $model, string $relation) {
    $class = $model::class;

    info("尝试在模型 [{$class}] 上懒惰加载 [{$relation}]。");
});

插入和更新相关模型

save 方法

Eloquent 提供了方便的方法来将新模型添加到关系中。例如,也许您需要向帖子添加新评论。您可以使用关系的 save 方法插入评论,而无需手动设置 post_id 属性:

php
use App\Models\Comment;
use App\Models\Post;

$comment = new Comment(['message' => '一条新评论。']);

$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' => '一条新评论。']),
    new Comment(['message' => '另一条新评论。']),
]);

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

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

$post->refresh();

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

递归保存模型和关系

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

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

$post->comments[0]->message = '消息';
$post->comments[0]->author->name = '作者名称';

$post->push();

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

php
$post->pushQuietly();

create 方法

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

php
use App\Models\Post;

$post = Post::find(1);

$comment = $post->comments()->create([
    'message' => '一条新评论。',
]);

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

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

$post->comments()->createMany([
    ['message' => '一条新评论。'],
    ['message' => '另一条新评论。'],
]);

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

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

$user->posts()->createQuietly([
    'title' => '帖子标题。',
]);

$user->posts()->createManyQuietly([
    ['title' => '第一篇帖子。'],
    ['title' => '第二篇帖子。'],
]);

您还可以使用 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 方法更新子模型时,父模型的时间戳才会被更新。