Skip to content

Eloquent: 关系

介绍

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

定义关系

Eloquent 关系被定义为 Eloquent 模型类上的方法。由于关系也可以作为强大的查询构建器,将关系定义为方法提供了强大的方法链和查询功能。例如,我们可以在 posts 关系上链式添加额外的查询约束:

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

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

一对一 / 拥有一个

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

php
<?php

namespace App\Models;

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

class User extends Model
{
    /**
     * 获取与用户关联的电话。
     */
    public function phone(): HasOne
    {
        return $this->hasOne(Phone::class);
    }
}

传递给 hasOne 方法的第一个参数是相关模型类的名称。一旦定义了关系,我们可以使用 Eloquent 的动态属性检索相关记录。动态属性允许您像访问模型上定义的属性一样访问关系方法:

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

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

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

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

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

定义关系的反向

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

php
<?php

namespace App\Models;

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

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

调用 user 方法时,Eloquent 将尝试查找一个 User 模型,其 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' => '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();

拥有多个中的一个

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

将“多”关系转换为 Has One 关系

通常,当使用 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');
}

您还可以使用 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());
    });
}

通过一个拥有

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

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

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 方法并提供这些关系的名称来流畅地定义“has-one-through”关系。这种方法的优点是可以重用已在现有关系上定义的键约定:

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

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

通过多个拥有

“has-many-through”关系提供了一种通过中间关系访问远程关系的便捷方式。例如,假设我们正在构建一个类似于 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 方法并提供这些关系的名称来流畅地定义“has-many-through”关系。例如,如果 Application 模型有一个 environments 关系,而 Environment 模型有一个 deployments 关系,您可以像这样定义连接应用程序和部署的“has-many-through”关系:

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 方法并提供这些关系的名称来流畅地定义“has-many-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' => 'Featured Post']);

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

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

检索中间表列

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

php
use App\Models\User;

$user = User::find(1);

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

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

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

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

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

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

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 子句约束到查询,但在通过定义的关系创建新模型时不会添加指定的值。如果您需要同时查询和创建具有特定 pivot 值的关系,可以使用 withPivotValue 方法:

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

通过中间表列排序查询

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

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

定义自定义中间表模型

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

自定义多对多 pivot 模型应扩展 Illuminate\Database\Eloquent\Relations\Pivot 类,而自定义多态多对多 pivot 模型应扩展 Illuminate\Database\Eloquent\Relations\MorphPivot 类。例如,我们可以定义一个使用自定义 RoleUser pivot 模型的 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 trait。如果您需要软删除 pivot 记录,请考虑将您的 pivot 模型转换为实际的 Eloquent 模型。

自定义 Pivot 模型和自增 ID

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

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

多态关系

多态关系允许子模型使用单个关联属于多种类型的模型。例如,假设您正在构建一个允许用户分享博客文章和视频的应用程序。在这样的应用程序中,一个 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_type列由Eloquent用于确定在访问imageable关系时返回哪种“类型”的父模型。在这种情况下,该列将包含App\Models\PostApp\Models\User

模型结构

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

php
<?php

namespace App\Models;

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

class Image extends Model
{
    /**
     * 获取父imageable模型(用户或帖子)。
     */
    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
{
    /**
     * 获取父commentable模型(帖子或视频)。
     */
    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

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

多对多(多态)

表结构

多对多多态关系比“morph one”和“morph many”关系稍微复杂一些。例如,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模型都将包含一个调用基Eloquent模型类提供的morphToMany方法的tags方法。

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。通过这样做,即使模型被重命名,数据库中的多态“类型”列值也将保持有效:

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)

关系方法与动态属性

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

php
use App\Models\User;

$user = User::find(1);

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

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

查询关系存在

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

php
use App\Models\Post;

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

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

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

嵌套的has语句可以使用“点”符号构造。例如,你可以检索所有至少有一个带有图像的评论的帖子:

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

如果你需要更强大的功能,可以使用whereHasorWhereHas方法在has查询上定义其他查询约束,例如检查评论的内容:

php
use Illuminate\Database\Eloquent\Builder;

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

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

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

查询关系缺失

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

php
use App\Models\Post;

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

如果你需要更强大的功能,可以使用whereDoesntHaveorWhereDoesntHave方法在doesntHave查询上添加其他查询约束,例如检查评论的内容:

php
use Illuminate\Database\Eloquent\Builder;

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

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

php
use Illuminate\Database\Eloquent\Builder;

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

查询“morph to”关系

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

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

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

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

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

php
use Illuminate\Database\Eloquent\Builder;

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

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

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

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

查询所有相关模型

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

php
use Illuminate\Database\Eloquent\Builder;

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

聚合相关模型

计数相关模型

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

php
use App\Models\Post;

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

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

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

php
use Illuminate\Database\Eloquent\Builder;

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

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

你还可以为关系计数结果设置别名,允许对同一关系进行多次计数:

php
use Illuminate\Database\Eloquent\Builder;

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

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

延迟计数加载

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

php
$book = Book::first();

$book->loadCount('genres');

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

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

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

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

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

其他聚合函数

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

php
use App\Models\Post;

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

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

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

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

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

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

php
$post = Post::first();

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

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

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

计数“morph to”关系的相关模型

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

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

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

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

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

延迟计数加载

假设我们已经检索了一组ActivityFeed模型,现在我们希望加载与活动提要关联的各种parentable模型的嵌套关系计数。你可以使用loadMorphCount方法来实现这一点:

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

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

预加载

当将Eloquent关系作为属性访问时,相关模型是“延迟加载”的。这意味着关系数据实际上是在你第一次访问属性时加载的。然而,Eloquent可以在查询父模型时“预加载”关系。预加载缓解了“N + 1”查询问题。为了说明N + 1查询问题,考虑一个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;
use Illuminate\Contracts\Database\Eloquent\Builder;

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

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

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

约束morphTo关系的预加载

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

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

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

在此示例中,Eloquent将仅预加载未隐藏的帖子和类型为“educational”的视频。

使用关系存在约束预加载

有时你可能需要在同时加载关系的情况下检查关系的存在。例如,你可能希望仅检索具有匹配给定查询条件的子Post模型的User模型,同时也预加载匹配的帖子。你可以使用withWhereHas方法实现这一点:

php
use App\Models\User;

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

延迟预加载

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

php
use App\Models\Book;

$books = Book::all();

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

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

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

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

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

嵌套延迟预加载和 morphTo

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

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

php
<?php

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

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

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

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

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

自动预加载

WARNING

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

在许多情况下,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 会自动为用户集合中的所有用户 延迟预加载 文章。同样地,当你尝试访问某篇文章的评论时,Laravel 也会为最初检索到的所有文章延迟预加载其评论。

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

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::handleLazyLoadingViolationUsing(function (Model $model, string $relation) {
    $class = $model::class;

    info("Attempted to lazy load [{$relation}] on model [{$class}].");
});

插入和更新相关模型

save 方法

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

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

$comment = new Comment(['message' => 'A new comment.']);

$post = Post::find(1);

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

请注意,我们没有将 comments 关系作为动态属性访问。相反,我们调用了 comments 方法以获取关系的实例。save 方法将自动将适当的 post_id 值添加到新的 Comment 模型中。

如果您需要保存多个相关模型,可以使用 saveMany 方法:

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

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

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

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

$post->refresh();

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

递归保存模型和关系

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

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

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

$post->push();

pushQuietly 方法可以用于保存模型及其关联关系而不触发任何事件:

php
$post->pushQuietly();

create 方法

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

php
use App\Models\Post;

$post = Post::find(1);

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

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

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

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

createQuietlycreateManyQuietly 方法可以用于创建模型而不触发任何事件:

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

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

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

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

NOTE

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

属于关系

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

php
use App\Models\Account;

$account = Account::find(10);

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

$user->save();

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

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

$user->save();

多对多关系

附加/分离

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

php
use App\Models\User;

$user = User::find(1);

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

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

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

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

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

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

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

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

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

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

同步关联

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

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

您还可以与 ID 一起传递附加的中间表值:

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

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

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

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

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

切换关联

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

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

您还可以与 ID 一起传递附加的中间表值:

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

更新中间表上的记录

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

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

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

更新父级时间戳

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

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

php
<?php

namespace App\Models;

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

class Comment extends Model
{
    /**
     * 所有应触摸的关系。
     *
     * @var array
     */
    protected $touches = ['post'];

    /**
     * 获取评论所属的帖子。
     */
    public function post(): BelongsTo
    {
        return $this->belongsTo(Post::class);
    }
}

WARNING

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