Skip to content

Eloquent 关系

介绍

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

定义关系

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

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

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

One to One

一对一关系是一种非常基本的数据库关系类型。例如,一个 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 将尝试查找 idPhone 模型上的 user_id 列匹配的 User 模型。

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 将采用父模型的 “snake case” 名称,并在其后缀中加上 _id。因此,在此示例中,Eloquent 将假设 Comment 模型上的外键列为 post_id

定义 relationship 方法后,我们可以通过访问 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 将尝试查找 idComment 模型上的 post_id 列匹配的 Post 模型。

Eloquent 通过检查关系方法的名称并在方法名称后加上 _ 后跟父模型主键列的名称来确定默认外键名称。因此,在此示例中,Eloquent 将假设 Post 模型在评论表上的外键是 post_id

但是,如果您的关系的外键不遵循这些约定,则可以将自定义外键名称作为第二个参数传递给 belongsTo 方法:

php
    /**
     * Get the post that owns the comment.
     */
    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');
    }

默认模型

belongsTohasOne``、hasOneThroughmorphOne 关系允许您定义一个默认模型,如果给定关系为 null,则返回该模型。此模式通常称为 Null Object 模式,可以帮助删除代码中的条件检查。在下面的示例中,如果没有用户附加到 Post 模型,则用户关系将返回一个空的 App\Models\User 模型:

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

查询属于关系

当查询 “belongs to” 关系的子项时,你可以手动构建 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();

Has One of Many

有时,一个模型可能具有许多相关模型,但您希望轻松检索关系的“最新”或“最旧”相关模型。例如,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');
}

WARNING

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

将 “many” relationships 转换为 Has one relationships

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

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

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

Advanced Has One of Many Relationships

可以构建更高级的 “has one of many” 关系。例如,产品模型可能具有许多关联的 Price 模型,即使在发布新定价后,这些模型仍保留在系统中。此外,商品的新定价数据可能能够通过published_at列提前发布,以便在将来的某个日期生效。

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

php
/**
 * Get the current pricing for the product.
 */
public function currentPricing(): HasOne
{
    return $this->hasOne(Price::class)->ofMany([
        'published_at' => 'max',
        'id' => 'max',
    ], function (Builder $query) {
        $query->where('published_at', '<', now());
    });
}

Has One Through

“has-one-through” 关系定义与另一个模型的一对一关系。但是,此关系表明,通过继续执行第三个模型,声明模型可以与另一个模型的一个实例匹配。

例如,在汽车维修店应用程序中,每个 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 方法并提供这些关系的名称来流畅地定义“has-one-through”关系。例如,如果 Mechanic 模型具有 cars 关系,而 Car 模型具有 owner 关系,则可以定义连接 mechan 和 owner 的“has-one-through”关系,如下所示:

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

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

关键约定

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

php
    class Mechanic extends Model
    {
        /**
         * Get the car's owner.
         */
        public function carOwner(): HasOneThrough
        {
            return $this->hasOneThrough(
                Owner::class,
                Car::class,
                'mechanic_id', // 汽车桌上的外键...
                'car_id', // owners 表上的外键...
                'id', // 机制表上的本地键...
                'id' // 本地键...
            );
        }
    }

或者,如前所述,如果已在关系中涉及的所有模型上定义了相关关系,则可以通过调用 through 方法并提供这些关系的名称来流畅地定义“has-one-through”关系。此方法提供了重用已在现有关系上定义的关键约定的优势:

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

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

Has Many Through

“has-many-through” 关系提供了一种通过中间关系访问远程关系的便捷方法。例如,假设我们正在构建一个像 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 方法并提供这些关系的名称来流畅地定义“has-many-through”关系。例如,如果 Project 模型具有 environments 关系,而 Environment 模型具有 deployments 关系,则可以定义一个 “has-many-through” 关系来连接 project 和 deployment,如下所示:

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', // 环境表上的外键...
                'environment_id', // 部署表上的外键...
                'id', // projects 表上的本地键...
                'id' // 本地键...
            );
        }
    }

或者,如前所述,如果已在关系中涉及的所有模型上定义了相关关系,则可以通过调用 through 方法并提供这些关系的名称来流畅地定义“has-many-through”关系。此方法提供了重用已在现有关系上定义的关键约定的优势:

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

// Dynamic syntax...
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 方法的结果。为了完成我们的 user / role 示例,让我们在 Role model 上定义 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 model 都会自动分配一个 pivot 属性。此属性包含表示中间表的模型。

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

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

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

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

WARNING

使用 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 方法筛选 belongsToMany 关系查询返回的结果:

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 方法。自定义 pivot 模型使您有机会在 pivot 模型上定义其他行为,例如方法和强制转换。

自定义多对多透视模型应扩展该类, 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
    {
        // ...
    }

WARNING

Pivot 模型不能使用 SoftDeletes 特征。如果您需要软删除转置记录,请考虑将转置模型转换为实际的 Eloquent 模型。

自定义透视模型和递增 ID

如果您定义了使用自定义透视模型的多对多关系,并且该透视模型具有自动递增的主键,则应确保自定义透视模型类定义设置为 true递增属性。

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

多态关系

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

一对一(多态性)

Table Structure

一对一多态关系类似于典型的一对一关系;但是,子模型可以属于使用单个关联的多种类型的模型。例如,blog 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 列来确定在访问可成像关系时要返回哪种父模型的“类型”。在这种情况下,该列将包含 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 dynamic relationship 属性:

php
    use App\Models\Post;

    $post = Post::find(1);

    $image = $post->image;

您可以通过访问执行对 morphTo 的调用的方法的名称来检索多态模型的父级。在本例中,这是 Image 模型上的 imageable 方法。因此,我们将该方法作为 dynamic relationship 属性进行访问:

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

一对多 (多态)

表结构

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

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 模型上的可评论关系将返回 PostVideo 实例,具体取决于哪种类型的模型是评论的父级。

One of Many (Polymorphic)

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

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

同样,您可以定义一个方法来检索关系的 “oldest” 或 first related model:

php
/**
 * Get the user's oldest image.
 */
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');
}

NOTE

可以构建更高级的 “one of many” 关系。有关更多信息,请查阅 has one of many 文档

多对多 (多态)

表结构

多对多多态关系比 “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

NOTE

在深入研究多态多对多关系之前,阅读有关典型多对多关系的文档可能会有所帮助。relationships](#many-to-many).

模型结构

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

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

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 方法接受相关模型的名称以及 “relationship name”。根据我们分配给中间表名称的名称及其包含的键,我们将这种关系称为 “taggable”:

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 将使用完全限定的类名来存储相关模型的 “type”。例如,在上面的一对多关系示例中,Comment 模型可能属于 PostVideo 模型,则默认commentable_type将分别是 App\Models\PostApp\Models\Video。但是,您可能希望将这些值与应用程序的内部结构分离。

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

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

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

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

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

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

    $alias = $post->getMorphClass();

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

WARNING

当向现有应用程序添加 “morph map” 时,数据库中每个仍然包含完全限定类的可 morphable *_type 列值都需要转换为其 “map” 名称。

动态关系

你可以使用 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');
    });

WARNING

在定义动态关系时,请始终为 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
    {
        /**
         * Get all of the posts for the user.
         */
        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();

您还可以指定 operator 和 count 值以进一步自定义查询:

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

Nested has 语句可以使用 “dot” 表示法构造。例如,您可以检索至少有一条评论至少具有一张图片的所有文章:

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

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

WARNING

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

内联关系存在查询

如果要使用附加到关系查询的单个简单 where 条件来查询关系的存在,您可能会发现使用 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();

您可能偶尔需要根据相关多态模型的 “type” 添加查询约束。传递给 whereHasMorph 方法的闭包可能会接收 $type 值作为其第二个参数。此参数允许您检查正在构建的查询的 “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();

查询所有相关模型

您可以提供 * 作为通配符值,而不是传递可能的多态模型数组。这将指示 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 方法,您可以为多个关系添加 “counts”,并为查询添加其他约束:

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

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

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

关系计数和自定义 Select 语句

如果要将 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 的“变形为”关系,该关系允许我们检索给定 ActivityFeed 实例的父 PhotoPost 模型。此外,我们假设照片模型“具有许多”标签模型,而文章模型“具有许多”评论模型。

现在,让我们假设我们想要检索 ActivityFeed 实例,并预先加载每个 ActivityFeed 实例的可父级父级模型。此外,我们还希望检索与每张父照片关联的标签数量以及与每个父帖子关联的评论数量:

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

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

延迟计数加载

假设我们已经检索了一组 ActivityFeed 模型,现在我们想要加载与活动源关联的各种父级模型的嵌套关系计数。你可以使用 loadMorphCount 方法来完成这个任务:

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

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

预先加载

当将 Eloquent 关系作为属性访问时,相关模型是 “延迟加载” 的。这意味着在您首次访问属性之前,不会实际加载关系数据。但是,Eloquent 可以在您查询父模型时 “预先加载” 关系。预先加载缓解了 “N + 1” 查询问题。为了说明 N + 1 查询问题,请考虑“属于”Author 模型的 Book 模型:

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 个额外的查询来检索每本书的作者。

值得庆幸的是,我们可以使用 Eager loading 将此操作减少到只有两个查询。在构建查询时,您可以使用 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();

嵌套的 Eager Loading 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 模型实例,并预先加载所有可父模型及其各自的嵌套关系:

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

WARNING

使用此功能时,应始终在要检索的列列表中包含 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();

约束 Eager Loads

有时,您可能希望预先加载关系,但也希望为预先加载查询指定其他查询条件。您可以通过将关系数组传递给 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 只会预先加载帖子的标题列包含单词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 将仅预先加载尚未隐藏的帖子和 type 值为 “educational” 的视频。

用 Relationship Existence 约束 Eager Loads

有时,您可能会发现自己需要检查是否存在关系,同时根据相同的条件加载关系。例如,您可能希望仅检索具有与给定查询条件匹配的子 Post 模型的 User 模型,同时还预先加载匹配的帖子。您可以使用 withWhereHas 方法完成此操作:

php
    use App\Models\User;

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

Lazy Eager Loading

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

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

$book->loadMissing('author');

Nested Lazy Eager Loading and 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 模型实例,并预先加载所有可父模型及其各自的嵌套关系:

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

防止延迟加载

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

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("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;

递归保存模型和关系

如果要保存模型及其所有相关关系,可以使用 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 数组。新创建的模型将由 create 方法返回:

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 方法可用于创建模型,而无需 dispatch 任何事件:

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

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

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

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

NOTE

在使用 create 方法之前,请务必查看 mass assignment 文档。

属于关系

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

php
    use App\Models\Account;

    $account = Account::find(10);

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

    $user->save();

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

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

    $user->save();

多对多关系

Attaching / Detaching

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

php
    use App\Models\User;

    $user = User::find(1);

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

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

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

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

php
    // Detach a single role from the user...
    $user->roles()->detach($roleId);

    // Detach all roles from the user...
    $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 方法。此方法接受 intermediate record 外键和要更新的属性数组:

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

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

Touching Parent Timestamps

当一个模型定义与另一个模型(例如属于 PostComment)的 belongsTobelongsToMany 关系时,在更新子模型时更新父模型的时间戳有时很有帮助。

例如,当 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);
        }
    }

WARNING

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