laravel
8. Eloquent ORM
Eloquent:关系

介绍

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

定义关系

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

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

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

一对一

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

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

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

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

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

此外,如果您希望关系使用除id或模型的$primaryKey属性以外的主键值,您可以向hasOne方法传递第三个参数:

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

定义关系的反向

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

<?php

namespace App\Models;

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

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

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

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

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

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

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

一对多

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

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

use App\Models\Post;

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

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

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

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

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

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

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

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

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

$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

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模型:

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

一对多(反向)/属于

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

<?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“动态关系属性”来检索评论的父文章:

use App\Models\Comment;

$comment = Comment::find(1);

return $comment->post->title;

在上面的示例中,Eloquent 将尝试找到一个idComment模型上的post_id列匹配的Post模型。

Eloquent 通过检查关系方法的名称并在方法名称后附加一个_和父模型主键列的名称来确定默认外键名称。因此,在这个例子中,Eloquent 将假定comments表上Post模型的外键是post_id

但是,如果您的关系的外键不符合这些约定,您可以向belongsTo方法传递一个自定义外键名称作为第二个参数:

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

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

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

默认模型

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

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

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

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

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

查询属于关系

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

use App\Models\Post;

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

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

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

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

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

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

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

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

拥有多个中的一个

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

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

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

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

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

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

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

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

将“多”关系转换为HasOne关系

当使用latestOfManyoldestOfManyofMany方法检索单个模型时,您通常已经为同一模型定义了一个“has many”关系。为了方便起见,Laravel 允许

定义关系的逆关系

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

<?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 属性访问中间表:

use App\Models\User;
 
$user = User::find(1);
 
foreach ($user->roles as $role) {
    echo $role->pivot->created_at;
}

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

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

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

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

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

[!WARNING]
利用 Eloquent 自动维护的时间戳的中间表需要同时具有 created_atupdated_at 时间戳列。

自定义 pivot 属性名称

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

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

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

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

$users = User::with('podcasts')->get();
 
foreach ($users->flatMap->podcasts as $podcast) {
    echo $podcast->subscription->created_at;
}

通过中间表列过滤查询

您还可以在定义关系时使用 wherePivotwherePivotInwherePivotNotInwherePivotBetweenwherePivotNotBetweenwherePivotNullwherePivotNotNull 方法来过滤 belongsToMany 关系查询返回的结果:

return $this->belongsToMany(Role::class)
                ->wherePivot('approved', 1);
 
return $this->belongsToMany(Role::class)
                ->wherePivotIn('priority', [1, 2]);
 
return $this->belongsToMany(Role::class)
                ->wherePivotNotIn('priority', [1, 2]);
 
return $this->belongsToMany(Podcast::class)
                ->as('subscriptions')
                ->wherePivotBetween('created_at', ['2020 - 01 - 01 00:00:00', '2020 - 12 - 31 00:00:00']);
 
return $this->belongsToMany(Podcast::class)
                ->as('subscriptions')
                ->wherePivotNotBetween('created_at', ['2020 - 01 - 01 00:00:00', '2020 - 12 - 31 00:00:00']);
 
return $this->belongsToMany(Podcast::class)
                ->as('subscriptions')
                ->wherePivotNull('expired_at');
 
return $this->belongsToMany(Podcast::class)
                ->as('subscriptions')
                ->wherePivotNotNull('expired_at');

通过中间表列对查询进行排序

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

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

定义自定义中间表模型

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

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

<?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
 
namespace App\Models;
 
use Illuminate\Database\Eloquent\Relations\Pivot;
 
class RoleUser extends Pivot
{
    //...
}

[!WARNING]
中间表模型不能使用 SoftDeletes 特性。如果您需要软删除中间表记录,请考虑将您的中间表模型转换为实际的 Eloquent 模型。

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

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

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

多态关系

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

一对一(多态)

表结构

一对一多态关系类似于典型的一对一关系;然而,子模型可以通过单个关联属于多种类型的模型。例如,博客 PostUser 可能与 Image 模型存在多态关系。使用一对一多态关系允许您拥有一个唯一图像的单个表,该表可以与文章和用户相关联。首先,让我们检查构建此关系所需的表结构:

posts
    id - integer
    name - string
 
users
    id - integer
    name - string
 
images
    id - integer
    url - string
    imageable_id - integer
    imageable_type - string

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

模型结构

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

<?php
 
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 动态关系属性:

use App\Models\Post;
 
$post = Post::find(1);
 
$image = $post->image;

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

use App\Models\Image;
 
$image = Image::find(1);
 
$imageable = $image->imageable;

Image 模型上的 imageable 关系将根据拥有该图像的模型类型返回 PostUser 实例。

关键约定

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

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

一对多(多态)

表结构

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

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
 
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 动态属性:

use App\Models\Post;
 
$post = Post::find(1);
 
foreach ($post->comments as $comment) {
    //...
}

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

use App\Models\Comment;
 
$comment = Comment::find(1);
 
$commentable = $comment->commentable;

Comment 模型上的 commentable 关系将根据评论的父模型类型返回 PostVideo 实例。

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

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

$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 在其子模型上自动填充父模型,您可以在定义 hasMany 关系时调用 chaperone 方法:

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

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

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

一对多中的一个(多态)

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

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

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

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

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

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

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

[!NOTE]
可以构建更

查询不存在的关系

在检索模型记录时,您可能希望根据不存在的关系来限制结果。例如,假设您想要检索所有没有任何评论的博客文章。为此,您可以将关系的名称传递给doesntHaveorDoesntHave方法:

use App\Models\Post;

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

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

use Illuminate\Database\Eloquent\Builder;

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

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

use Illuminate\Database\Eloquent\Builder;

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

查询多态关系

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

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值作为其第二个参数。这个参数允许您检查正在构建的查询的“类型”:

use Illuminate\Database\Eloquent\Builder;

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

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

查询所有相关模型

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

use Illuminate\Database\Eloquent\Builder;

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

聚合相关模型

计算相关模型的数量

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

use App\Models\Post;

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

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

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

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;

您还可以为关系计数结果设置别名,允许在同一关系上进行多个计数:

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方法,您可以在已经检索到父模型后加载关系计数:

$book = Book::first();

$book->loadCount('genres');

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

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

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

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

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

其他聚合函数

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

use App\Models\Post;

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

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

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

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

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

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

$post = Post::first();

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

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

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

在多态到关系上计算相关模型的数量

如果您想要急切加载“多态到”关系,以及该关系可能返回的各种实体的相关模型计数,您可以结合使用with方法和morphTo关系的morphWithCount方法。

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

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

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方法来实现此目的:

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

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

急切加载

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

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

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

use App\Models\Book;

$books = Book::all();

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

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

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

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

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

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

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

急切加载多个关系

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

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

嵌套急切加载

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

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

或者,您可以通过向with方法提供一个嵌套数组来指定嵌套的急切加载关系,当急切加载多个嵌套关系时,这可能很方便:

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

嵌套急切加载 morphTo 关系

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

<?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模型及其各自的嵌套关系:

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 允许您指定您想要检索关系的哪些列:

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

[!WARNING]
使用此功能时,您应始终在要检索的列列表中包含id列以及任何相关的外键列。

默认急切加载

有时您可能希望在检索模型时始终加载一些关系。要实现此目的,您可以在模型上定义一个$with属性:

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

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

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

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

约束急切加载

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

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

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

在这个例子中,Eloquent 将仅急切加载标题列中包含单词code的文章。您可以调用其他查询构建器方法来进一步自定义急切加载操作:

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

约束 morphTo 关系的急切加载

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

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方法来实现此目的:

use App\Models\User;

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

懒急切加载

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

use App\Models\Book;

$books = Book::all();

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

如果您需要为急切加载查询设置其他查询约束,可以传递一个以您希望加载的关系为键的数组。数组值应该是接收