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 将查找用户 id 列的值,该值位于 Phone 记录的 user_id 列中。如果你希望关联使用除 id 或模型主键之外的主键值,可以向 hasOne 方法传递第三个参数:

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

定义反向关联

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

php
<?php

namespace App\Models;

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

class Phone extends Model
{
    /**
     * 获取拥有此电话的用户。
     */
    public function user(): BelongsTo
    {
        return $this->belongsTo(User::class);
    }
}

调用 user 方法时,Eloquent 将尝试找到一个 User 模型,其 idPhone 模型上的 user_id 列匹配。

Eloquent 通过检查关联方法的名称,并将方法名称后加上 _id 来确定外键名称。因此,在这种情况下,Eloquent 假定 Phone 模型有一个 user_id 列。但是,如果 Phone 模型上的外键不是 user_id,你可以将自定义键名作为第二个参数传递给 belongsTo 方法:

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

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

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

一对多 / 拥有多个

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

php
<?php

namespace App\Models;

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

class Post extends Model
{
    /**
     * 获取博客文章的评论。
     */
    public function comments(): HasMany
    {
        return $this->hasMany(Comment::class);
    }
}

请记住,Eloquent 将自动确定 Comment 模型的正确外键列。按照约定,Eloquent 将采用父模型的“蛇形”名称,后跟 _id。因此,在本例中,Eloquent 将假定 Comment 模型上的外键列是 post_id

一旦定义了关联方法,我们就可以通过访问 comments 属性来获取关联评论的集合。记住,由于 Eloquent 提供了“动态关联属性”,我们可以像访问模型上定义的属性一样访问关联方法:

php
use App\Models\Post;

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

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

由于所有关联也可以作为查询构建器使用,你可以通过调用 comments 方法并继续在查询上链式添加条件来为关联查询添加更多约束:

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

hasOne 方法一样,你也可以通过向 hasMany 方法传递额外参数来覆盖外键和本地键:

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

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

在子模型上自动填充父模型

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

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

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

在上面的示例中,引入了一个“N + 1”查询问题,因为即使为每个 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 将尝试找到一个 Post 模型,其 idComment 模型上的 post_id 列匹配。

Eloquent 通过检查关联方法的名称,并将方法名称后加上 _ 以及父模型主键列的名称来确定默认外键名称。因此,在此示例中,Eloquent 将假定 comments 表上的 Post 模型的外键是 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,则返回该默认模型。这种模式通常被称为空对象模式,可以帮助你删除代码中的条件检查。在以下示例中,如果没有用户关联到 Post 模型,user 关联将返回一个空的 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' => '访客作者',
    ]);
}

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

查询属于关联

当查询“属于”关系的子模型时,你可以手动构建 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');
}

WARNING

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

你也可以使用 one 方法将 HasManyThrough 关联转换为 HasOneThrough 关联:

php
public function latestDeployment(): HasOneThrough
{
    return $this->deployments()->one()->latestOfMany();
}

高级一对一最新关联

可以构建更高级的“一对一最新”关系。例如,一个 Product 模型可能有许多关联的 Price 模型,即使在新价格发布后,旧价格仍保留在系统中。此外,产品的新价格数据可以提前发布,以便通过 published_at 列在未来的日期生效。

因此,简而言之,我们需要检索已发布的最新价格,且发布日期不在将来。另外,如果两个价格具有相同的发布日期,我们将选择 ID 最大的价格。要实现这一点,我们必须向 ofMany 方法传递一个数组,其中包含确定最新价格的可排序列。此外,一个闭包将作为第二个参数提供给 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 模型访问车主。让我们看一下定义此关系所需的表:

text
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', // cars 表上的外键...
            'car_id', // owners 表上的外键...
            'id', // mechanics 表上的本地键...
            'id' // cars 表上的本地键...
        );
    }
}

或者,如前所述,如果相关关系已经在关系涉及的所有模型上定义好了,你可以通过调用 through 方法并提供这些关系的名称来流畅地定义“远程一对一”关系。这种方法具有重用现有关系上已定义的键约定的优势:

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

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

远程一对多

“远程一对多”关系提供了通过中间关系访问远距离关系的便捷方式。例如,假设我们正在构建一个类似 Laravel Cloud 的部署平台。一个 Application 模型可以通过中间 Environment 模型访问许多 Deployment 模型。使用此示例,你可以轻松地收集给定应用程序的所有部署。让我们看一下定义此关系所需的表:

text
applications
    id - integer
    name - string

environments
    id - integer
    application_id - integer
    name - string

deployments
    id - integer
    environment_id - integer
    commit_hash - string

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

php
<?php

namespace App\Models;

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

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

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

或者,如果相关关系已经在关系涉及的所有模型上定义好了,你可以通过调用 through 方法并提供这些关系的名称来流畅地定义“远程一对多”关系。例如,如果 Application 模型有一个 environments 关系,而 Environment 模型有一个 deployments 关系,你可以像这样定义连接应用程序和部署的“远程一对多”关系:

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

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

尽管 Deployment 模型的表不包含 application_id 列,但 hasManyThrough 关系通过 $application->deployments 提供了对应用程序部署的访问。为了检索这些模型,Eloquent 会检查中间 Environment 模型表上的 application_id 列。找到相关的环境 ID 后,它们被用于查询 Deployment 模型表。

键约定

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

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

或者,如前所述,如果相关关系已经在关系涉及的所有模型上定义好了,你可以通过调用 through 方法并提供这些关系的名称来流畅地定义“远程一对多”关系。这种方法具有重用现有关系上已定义的键约定的优势:

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

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

作用域关联

通常会给模型添加额外的方法来约束关系。例如,你可能会给 User 模型添加一个 featuredPosts 方法,该方法通过额外的 where 约束来限制更广泛的 posts 关系:

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

    /**
     * 获取用户的精选文章。
     */
    public function featuredPosts(): HasMany
    {
        return $this->posts()->where('featured', true);
    }
}

但是,如果你尝试通过 featuredPosts 方法创建模型,其 featured 属性不会被设置为 true。如果你希望通过关系方法创建模型,并指定应添加到通过该关系创建的所有模型中的属性,你可以在构建关系查询时使用 withAttributes 方法:

php
/**
 * 获取用户的精选文章。
 */
public function featuredPosts(): HasMany
{
    return $this->posts()->withAttributes(['featured' => true]);
}

withAttributes 方法将使用给定的属性为查询添加 where 条件,并且还会将给定的属性添加到通过该关系方法创建的任何模型中:

php
$post = $user->featuredPosts()->create(['title' => '精选文章']);

$post->featured; // true

要指示 withAttributes 方法不向查询添加 where 条件,你可以将 asConditions 参数设置为 false

php
return $this->posts()->withAttributes(['featured' => true], asConditions: false);

多对多关联关系

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

表结构

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

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

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

如你所见,除了引用 App\Models\User 模型之外,该关系的定义与其 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();

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

wherePivot 为查询添加了一个 where 子句约束,但不会在通过定义的关系创建新模型时添加指定的值。如果你需要使用特定的中间表值进行查询和创建关系,可以使用 withPivotValue 方法:

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

通过中间表字段排序查询

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

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

定义自定义中间表模型

如果你想定义一个自定义模型来表示多对多关系的中间表,可以在定义关系时调用 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
{
    // ...
}

WARNING

中间表模型不能使用 SoftDeletes trait。如果你需要软删除中间表记录,请考虑将中间表模型转换为实际的 Eloquent 模型。

自定义中间表模型和自增 ID

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

php
use Illuminate\Database\Eloquent\Attributes\Table;
use Illuminate\Database\Eloquent\Relations\Pivot;

#[Table(incrementing: true)]
class RoleUser extends Pivot
{
    // ...
}

多态关联关系

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

一对一(多态)

表结构

一对一多态关系类似于典型的一对一关系;但是,子模型可以使用单个关联属于多种类型的模型。例如,博客 PostUser 可以与 Image 模型共享多态关系。使用一对一多态关系,你可以拥有一个包含唯一图像的表,这些图像可以与文章和用户相关联。首先,让我们检查表结构:

text
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 列将包含父模型的类名。当访问 imageable 关系时,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 动态关联属性:

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 表来包含对文章和视频的评论。首先,让我们检查构建此关系所需的表结构:

text
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;
    }
}

在上面的示例中,引入了一个“N + 1”查询问题,因为即使为每个 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');
}

NOTE

可以构建更高级的“拥有最新一条记录”关系。有关更多信息,请查阅拥有最新一条记录文档

多对多(多态)

表结构

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

text
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

在深入多态多对多关系之前,你可能需要先阅读有关典型多对多关系的文档。

模型结构

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

morphToMany 方法接受相关模型的名称以及“关系名称”。根据我们为中间表名称及其包含的键指定的名称,我们将此关系称为“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 方法接受相关模型的名称以及“关系名称”。根据我们为中间表名称及其包含的键指定的名称,我们将此关系称为“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 将使用完全限定的类名来存储相关模型的“类型”。例如,在上述一对多多态关系示例中,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\AppServiceProvider 类的 boot 方法中调用 enforceMorphMap 方法,或者如果你愿意,可以创建一个单独的服务提供者。

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

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

$alias = $post->getMorphClass();

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

WARNING

在现有应用程序中添加“多态映射”时,数据库中每个可多态的 *_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');
});

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
{
    /**
     * 获取用户的所有文章。
     */
    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)

关联方法 vs. 动态属性

如果你不需要向 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
// 检索至少有 3 条评论的所有文章...
$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();

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

WARNING

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

多对多关系存在性查询

whereAttachedTo 方法可用于查询与某个模型或模型集合具有多对多关联的模型:

php
$users = User::whereAttachedTo($role)->get();

你也可以向 whereAttachedTo 方法提供一个集合实例。这样做时,Laravel 将检索附加到集合中任何模型的模型:

php
$tags = Tag::whereLike('name', '%laravel%')->get();

$posts = Post::whereAttachedTo($tags)->get();

内联关系存在性查询

如果你希望通过一个简单的 where 条件附加到关系查询来查询关系的存在性,你可能会发现使用 whereRelationorWhereRelationwhereMorphRelationorWhereMorphRelation 方法更方便。例如,我们可以查询所有具有未批准评论的文章:

php
use App\Models\Post;

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

当然,就像调用查询构建器的 where 方法一样,你也可以指定一个运算符:

php
$posts = Post::whereRelation(
    'comments', 'created_at', '>=', now()->minus(hours: 1)
)->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', 1);
})->get();

查询 MorphTo 关联

要查询“多态关联”的存在性,你可以使用 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();

有时你可能想要查询“多态关联”父级的子级。你可以使用 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);
}])

关系计数与自定义 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();

在 MorphTo 关联上统计关联模型

如果你想预加载一个“morphTo”关系,以及该关系可能返回的各种实体的相关模型计数,你可以将 with 方法与 morphTo 关系的 morphWithCount 方法结合使用。

在此示例中,我们假设 PhotoPost 模型可能创建 ActivityFeed 模型。我们将假设 ActivityFeed 模型定义了一个名为 parentable 的“morphTo”关系,允许我们检索给定 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 查询问题,考虑一个 Book 模型“属于”一个 Author 模型的情况:

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

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

为预加载添加约束

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

php
use App\Models\User;

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

在此示例中,Eloquent 只会预加载 title 列包含单词 code 的文章。你可以调用其他查询构建器方法来进一步自定义预加载操作:

php
$users = User::with(['posts' => function ($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”的视频。

结合关系存在性约束进行预加载

有时你可能发现自己需要检查关系是否存在,同时根据相同的条件加载该关系。例如,你可能希望只检索具有符合给定查询条件的子 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 ($condition) {
    $books->load('author', 'publisher');
}

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

php
$author->load(['books' => function ($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'],
    ]);

自动预加载

WARNING

此功能目前处于测试阶段,以收集社区反馈。此功能的行为和功能即使在补丁版本中也可能会发生变化。

在许多情况下,Laravel 可以自动预加载你访问的关系。要启用自动预加载,你应在应用程序的 AppServiceProviderboot 方法中调用 Model::automaticallyEagerLoadRelationships 方法:

php
use Illuminate\Database\Eloquent\Model;

/**
 * 引导任何应用程序服务。
 */
public function boot(): void
{
    Model::automaticallyEagerLoadRelationships();
}

启用此功能后,Laravel 将尝试自动加载你访问的任何尚未加载的关系。例如,考虑以下场景:

php
use App\Models\User;

$users = User::all();

foreach ($users as $user) {
    foreach ($user->posts as $post) {
        foreach ($post->comments as $comment) {
            echo $comment->content;
        }
    }
}

通常,上面的代码会为每个用户执行一个查询来检索他们的文章,并为每篇文章执行一个查询来检索其评论。但是,当启用了 automaticallyEagerLoadRelationships 功能后,当你尝试访问任何检索到的用户的文章时,Laravel 将自动为用户集合中的所有用户延迟预加载文章。同样,当你尝试访问任何检索到的文章的评论时,将为最初检索到的所有文章延迟预加载所有评论。

如果你不想全局启用自动预加载,你仍然可以通过在集合实例上调用 withRelationshipAutoloading 方法为单个 Eloquent 集合实例启用此功能:

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

return $users->withRelationshipAutoloading();

禁止延迟加载

如前所述,预加载关系通常可以为你的应用程序带来显著的性能优势。因此,如果你愿意,可以指示 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::handleLazyLoadingViolationsUsing(function (Model $model, string $relation) {
    $class = $model::class;

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

插入和更新关联模型

save 方法

Eloquent 提供了便捷的方法来向关系添加新模型。例如,你可能需要向一篇文章添加一条新评论。你可以使用关系的 save 方法插入评论,而不是手动在 Comment 模型上设置 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 arraycreate 方法将返回新创建的模型:

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 方法来在关系上创建和更新模型

NOTE

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

属于关联

如果你希望将子模型分配给新的父模型,可以使用 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();

多对多关联

附加 / 分离

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],
]);

事务性数据透视操作

上述每个数据透视操作都有一个 OrFail 变体(attachOrFaildetachOrFailsyncOrFailsyncWithoutDetachingOrFailtoggleOrFail),这些方法会将操作包装在一个数据库事务中,这样如果抛出异常,所有更改都会自动回滚:

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

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

更新中间表上的记录

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

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

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

更新父模型时间戳

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

例如,当更新 Comment 模型时,你可能希望自动“触及”所属 Postupdated_at 时间戳,以便将其设置为当前日期和时间。要实现此目的,你可以在子模型上使用 Touches 属性,其中包含在更新子模型时应更新其 updated_at 时间戳的关系的名称:

php
<?php

namespace App\Models;

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

#[Touches(['post'])]
class Comment extends Model
{
    /**
     * 获取该评论所属的文章。
     */
    public function post(): BelongsTo
    {
        return $this->belongsTo(Post::class);
    }
}

WARNING

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