雄辩:关系

Introduction

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

定义关系

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

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

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

一对一

一对一关系是一种非常基本的数据库关系类型。例如,一个User 模型可能与一个相关联Phone 模型。为了定义这种关系,我们将放置一个phone 上的方法User 模型。这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
{
    /**
     * Get the phone associated with the user.
     */
    public function phone(): HasOne
    {
        return $this->hasOne(Phone::class);
    }
}

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

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

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

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

此外,Eloquent 假定外键的值应与父项的主键列相匹配。换句话说,Eloquent 会寻找用户的价值id列中的user_id 列的Phone 记录。如果您希望关系使用主键值而不是id 或者你的模特$primaryKey 属性,您可以将第三个参数传递给hasOne 方法:

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

定义关系的倒数

所以,我们可以访问Phone 我们的模型User 模型。接下来,让我们定义一个关系Phone 允许我们访问拥有手机的用户的模型。我们可以定义 a 的逆hasOne 关系使用belongsTo 方法:

<?php

namespace App\Models;

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

class Phone extends Model
{
    /**
     * Get the user that owns the phone.
     */
    public function user(): BelongsTo
    {
        return $this->belongsTo(User::class);
    }
}

当调用user 方法,Eloquent 将尝试找到一个User 有一个模型id 匹配的user_id 专栏上的Phone 模型。

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

/**
 * Get the user that owns the phone.
 */
public function user(): BelongsTo
{
    return $this->belongsTo(User::class, 'foreign_key');
}

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

/**
 * Get the user that owns the phone.
 */
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
{
    /**
     * Get the comments for the blog post.
     */
    public function comments(): HasMany
    {
        return $this->hasMany(Comment::class);
    }
}

请记住,Eloquent 会自动确定正确的外键列Comment 模型。按照惯例,Eloquent 将采用父模型的“蛇形”名称并将其后缀为_id.所以,在这个例子中,Eloquent 将假定外键列在Comment 模型是post_id.

一旦定义了关系方法,我们就可以访问collection 通过访问相关评论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');

一对多(逆)/属于

现在我们可以访问帖子的所有评论,让我们定义一个关系以允许评论访问其父帖子。定义 a 的逆hasMany 关系,在调用子模型的子模型上定义一个关系方法belongsTo 方法:

<?php

namespace App\Models;

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

class Comment extends Model
{
    /**
     * Get the post that owns the comment.
     */
    public function post(): BelongsTo
    {
        return $this->belongsTo(Post::class);
    }
}

一旦定义了关系,我们就可以通过访问post “动态关系属性”:

use App\Models\Comment;

$comment = Comment::find(1);

return $comment->post->title;

在上面的例子中,Eloquent 会试图找到一个Post 有一个模型id 匹配的post_id 专栏上的Comment 模型。

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

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

/**
 * Get the post that owns the comment.
 */
public function post(): BelongsTo
{
    return $this->belongsTo(Post::class, 'foreign_key');
}

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

/**
 * Get the post that owns the comment.
 */
public function post(): BelongsTo
{
    return $this->belongsTo(Post::class, 'foreign_key', 'owner_key');
}

默认模型

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

/**
 * Get the author of the post.
 */
public function user(): BelongsTo
{
    return $this->belongsTo(User::class)->withDefault();
}

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

/**
 * Get the author of the post.
 */
public function user(): BelongsTo
{
    return $this->belongsTo(User::class)->withDefault([
        'name' => 'Guest Author',
    ]);
}

/**
 * Get the author of the post.
 */
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();

您还可以提供一个collection 实例到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 方法:

/**
 * Get the user's most recent order.
 */
public function latestOrder(): HasOne
{
    return $this->hasOne(Order::class)->latestOfMany();
}

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

/**
 * Get the user's oldest order.
 */
public function oldestOrder(): HasOne
{
    return $this->hasOne(Order::class)->oldestOfMany();
}

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

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

/**
 * Get the user's largest order.
 */
public function largestOrder(): HasOne
{
    return $this->hasOne(Order::class)->ofMany('price', 'max');
}

Warning
因为 PostgreSQL 不支持执行MAX 针对 UUID 列的函数,目前不可能结合 PostgreSQL UUID 列使用多之一关系。

高级具有许多关系之一

可以构建更高级的“拥有众多”关系。例如,一个Product 模型可能有很多关联Price 即使在发布新定价后仍保留在系统中的模型。此外,产品的新定价数据可能会提前发布,并通过published_at 柱子。

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

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

有一个通过

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

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

mechanics
    id - integer
    name - string

cars
    id - integer
    model - string
    mechanic_id - integer

owners
    id - integer
    name - string
    car_id - integer

现在我们已经检查了关系的表结构,让我们在Mechanic 模型:

<?php

namespace App\Models;

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

class Mechanic extends Model
{
    /**
     * Get the car's owner.
     */
    public function carOwner(): HasOneThrough
    {
        return $this->hasOneThrough(Owner::class, Car::class);
    }
}

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

或者,如果已经在关系中涉及的所有模型上定义了相关关系,您可以通过调用through 方法并提供这些关系的名称。例如,如果Mechanic 模型有一个cars关系和Car 模型有一个owner 关系,您可以像这样定义一个连接机械师和所有者的“has-one-through”关系:

// String based syntax...
return $this->through('cars')->has('owner');

// Dynamic syntax...
return $this->throughCars()->hasOwner();

主要约定

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

class Mechanic extends Model
{
    /**
     * Get the car's owner.
     */
    public function carOwner(): HasOneThrough
    {
        return $this->hasOneThrough(
            Owner::class,
            Car::class,
            'mechanic_id', // Foreign key on the cars table...
            'car_id', // Foreign key on the owners table...
            'id', // Local key on the mechanics table...
            'id' // Local key on the cars table...
        );
    }
}

或者,如前所述,如果已经在关系中涉及的所有模型上定义了相关关系,您可以通过调用through 方法并提供这些关系的名称。这种方法的优点是可以重用已在现有关系上定义的关键约定:

// String based syntax...
return $this->through('cars')->has('owner');

// Dynamic syntax...
return $this->throughCars()->hasOwner();

有很多通过

“has-many-through”关系提供了一种通过中间关系访问远距离关系的便捷方式。例如,假设我们正在构建一个部署平台,例如Laravel 蒸气. AProject 模型可能会访问很多Deployment 通过中间模型Environment 模型。使用此示例,您可以轻松地收集给定项目的所有部署。让我们看一下定义这种关系所需的表:

projects
    id - integer
    name - string

environments
    id - integer
    project_id - integer
    name - string

deployments
    id - integer
    environment_id - integer
    commit_hash - string

现在我们已经检查了关系的表结构,让我们在Project 模型:

<?php

namespace App\Models;

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

class Project extends Model
{
    /**
     * Get all of the deployments for the project.
     */
    public function deployments(): HasManyThrough
    {
        return $this->hasManyThrough(Deployment::class, Environment::class);
    }
}

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

或者,如果已经在关系中涉及的所有模型上定义了相关关系,您可以通过调用through 方法并提供这些关系的名称。例如,如果Project 模型有一个environments 关系和Environment 模型有一个deployments 关系,您可以定义一个连接项目和部署的“has-many-through”关系,如下所示:

// String based syntax...
return $this->through('environments')->has('deployments');

// Dynamic syntax...
return $this->throughEnvironments()->hasDeployments();

虽然Deployment 模型表不包含project_id 专栏,hasManyThrough 关系通过以下方式提供对项目部署的访问$project->deployments.为了检索这些模型,Eloquent 检查了project_id 中间柱Environment 模型表。找到相关的环境ID后,用于查询Deployment 模型表。

主要约定

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

class Project extends Model
{
    public function deployments(): HasManyThrough
    {
        return $this->hasManyThrough(
            Deployment::class,
            Environment::class,
            'project_id', // Foreign key on the environments table...
            'environment_id', // Foreign key on the deployments table...
            'id', // Local key on the projects table...
            'id' // Local key on the environments table...
        );
    }
}

或者,如前所述,如果已经在关系中涉及的所有模型上定义了相关关系,您可以通过调用through 方法并提供这些关系的名称。这种方法的优点是可以重用已在现有关系上定义的关键约定:

// String based syntax...
return $this->through('environments')->has('deployments');

// Dynamic syntax...
return $this->throughEnvironments()->hasDeployments();

多对多关系

多对多关系比hasOnehasMany 关系。多对多关系的一个示例是具有多个角色的用户,并且这些角色也由应用程序中的其他用户共享。例如,可以为用户分配“作者”和“编辑”角色;但是,这些角色也可以分配给其他用户。所以,一个用户有多个角色,一个角色有多个用户。

表结构

要定义这种关系,需要三个数据库表:users,roles, 和role_user.这role_user 表是根据相关模型名称的字母顺序派生的,并包含user_idrole_id 列。该表用作链接用户和角色的中间表。

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

users
    id - integer
    name - string

roles
    id - integer
    name - string

role_user
    user_id - integer
    role_id - integer

模型结构

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

<?php

namespace App\Models;

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

class User extends Model
{
    /**
     * The roles that belong to the user.
     */
    public function roles(): BelongsToMany
    {
        return $this->belongsToMany(Role::class);
    }
}

定义关系后,您可以使用roles 动态关系属性:

use App\Models\User;

$user = User::find(1);

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

由于所有关系也充当查询构建器,您可以通过调用roles 方法并继续将条件链接到查询:

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

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

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

除了自定义中间表的名称外,您还可以通过向belongsToMany 方法。第三个参数是您在其上定义关系的模型的外键名称,而第四个参数是您要加入的模型的外键名称:

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

定义关系的倒数

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

<?php

namespace App\Models;

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

class Role extends Model
{
    /**
     * The users that belong to the role.
     */
    public function users(): BelongsToMany
    {
        return $this->belongsToMany(User::class);
    }
}

如您所见,该关系的定义与它的完全相同User 模型对应物,除了引用App\Models\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');

如果你希望你的中间表有created_atupdated_at Eloquent 自动维护的时间戳,调用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;
}

通过中间表列过滤查询

您还可以过滤返回的结果belongsToMany 关系查询使用wherePivot,wherePivotIn,wherePivotNotIn,wherePivotBetween,wherePivotNotBetween,wherePivotNull, 和wherePivotNotNull 定义关系时的方法:

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

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

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

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
{
    /**
     * The users that belong to the role.
     */
    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

如果您已经定义了一个使用自定义数据透视模型的多对多关系,并且该数据透视模型具有自动递增的主键,您应该确保您的自定义数据透视模型类定义了一个incrementing 设置为的属性true.

/**
 * Indicates if the IDs are auto-incrementing.
 *
 * @var bool
 */
public $incrementing = true;

多态关系

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

一对一(多态)

表结构

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

posts
    id - integer
    name - string

users
    id - integer
    name - string

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

注意imageable_idimageable_type 上的列images 桌子。这imageable_id 列将包含帖子或用户的 ID 值,而imageable_type 列将包含父模型的类名。这imageable_type Eloquent 使用 column 来确定在访问imageable 关系。在这种情况下,该列将包含App\Models\Post 或者App\Models\User.

模型结构

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

<?php

namespace App\Models;

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

class Image extends Model
{
    /**
     * Get the parent imageable model (user or post).
     */
    public function imageable(): MorphTo
    {
        return $this->morphTo();
    }
}

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

class Post extends Model
{
    /**
     * Get the post's image.
     */
    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
{
    /**
     * Get the user's image.
     */
    public function image(): MorphOne
    {
        return $this->morphOne(Image::class, 'imageable');
    }
}

检索关系

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

use App\Models\Post;

$post = Post::find(1);

$image = $post->image;

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

use App\Models\Image;

$image = Image::find(1);

$imageable = $image->imageable;

imageable 上的关系Image 模型将返回Post 或者User 例如,取决于哪种类型的模型拥有图像。

主要约定

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

/**
 * Get the model that the image belongs to.
 */
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
{
    /**
     * Get the parent commentable model (post or video).
     */
    public function commentable(): MorphTo
    {
        return $this->morphTo();
    }
}

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

class Post extends Model
{
    /**
     * Get all of the post's comments.
     */
    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
{
    /**
     * Get all of the video's comments.
     */
    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.在这种情况下,这是commentable 上的方法Comment 模型。因此,我们将访问该方法作为动态关系属性以访问评论的父模型:

use App\Models\Comment;

$comment = Comment::find(1);

$commentable = $comment->commentable;

commentable 上的关系Comment 模型将返回Post 或者Video 例如,取决于哪种类型的模型是评论的父级。

许多之一(多态)

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

/**
 * Get the user's most recent image.
 */
public function latestImage(): MorphOne
{
    return $this->morphOne(Image::class, 'imageable')->latestOfMany();
}

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

/**
 * Get the user's oldest image.
 */
public function oldestImage(): MorphOne
{
    return $this->morphOne(Image::class, 'imageable')->oldestOfMany();
}

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

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

/**
 * Get the user's most popular image.
 */
public function bestImage(): MorphOne
{
    return $this->morphOne(Image::class, 'imageable')->ofMany('likes', 'max');
}

Note
可以构建更高级的“多选之一”关系。欲了解更多信息,请咨询有许多文件之一.

多对多(多态)

表结构

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

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 调用的方法morphToMany 由基础 Eloquent 模型类提供的方法。

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

<?php

namespace App\Models;

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

class Post extends Model
{
    /**
     * Get all of the tags for the post.
     */
    public function tags(): MorphToMany
    {
        return $this->morphToMany(Tag::class, 'taggable');
    }
}

定义关系的倒数

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

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

<?php

namespace App\Models;

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

class Tag extends Model
{
    /**
     * Get all of the posts that are assigned this tag.
     */
    public function posts(): MorphToMany
    {
        return $this->morphedByMany(Post::class, 'taggable');
    }

    /**
     * Get all of the videos that are assigned this tag.
     */
    public function videos(): MorphToMany
    {
        return $this->morphedByMany(Video::class, 'taggable');
    }
}

检索关系

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

use App\Models\Post;

$post = Post::find(1);

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

您可以通过访问执行调用的方法的名称,从多态子模型中检索多态关系的父级morphedByMany.在这种情况下,这是posts 或者videos 上的方法Tag 模型:

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\Post 或者App\Models\Video, 分别。但是,您可能希望将这些值与应用程序的内部结构分离。

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

use Illuminate\Database\Eloquent\Relations\Relation;

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

你可以打电话给enforceMorphMap 中的方法boot 你的方法App\Providers\AppServiceProvider 如果您愿意,可以分类或创建一个单独的服务提供者。

您可以在运行时使用模型的getMorphClass 方法。相反,您可以使用Relation::getMorphedModel 方法:

use Illuminate\Database\Eloquent\Relations\Relation;

$alias = $post->getMorphClass();

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

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

动态关系

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

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

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

namespace App\Models;

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

class User extends Model
{
    /**
     * Get all of the posts for the user.
     */
    public function posts(): HasMany
    {
        return $this->hasMany(Post::class);
    }
}

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

use App\Models\User;

$user = User::find(1);

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

你可以使用任何 Laravel查询生成器的 关系上的方法,因此请务必浏览查询生成器文档以了解所有可用的方法。

链接orWhere 关系后的条款

如上例所示,您可以在查询关系时随意向关系添加其他约束。但是,链接时要小心orWhere 从句到关系,作为orWhere 子句将在逻辑上分组为与关系约束相同的级别:

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

上面的示例将生成以下 SQL。如您所见,or 子句指示查询返回any 投票数超过 100 的用户。查询不再局限于特定用户:

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

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

use Illuminate\Database\Eloquent\Builder;

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

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

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

关系方法与。动态属性

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

use App\Models\User;

$user = User::find(1);

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

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

查询关系存在

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

use App\Models\Post;

// Retrieve all posts that have at least one comment...
$posts = Post::has('comments')->get();

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

// Retrieve all posts that have three or more comments...
$posts = Post::has('comments', '>=', 3)->get();

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

// Retrieve posts that have at least one comment with images...
$posts = Post::has('comments.images')->get();

如果你需要更多的力量,你可以使用whereHasorWhereHas 方法来定义你的额外查询约束has 查询,例如检查评论的内容:

use Illuminate\Database\Eloquent\Builder;

// Retrieve posts with at least one comment containing words like code%...
$posts = Post::whereHas('comments', function (Builder $query) {
    $query->where('content', 'like', 'code%');
})->get();

// Retrieve posts with at least ten comments containing words like code%...
$posts = Post::whereHas('comments', function (Builder $query) {
    $query->where('content', 'like', 'code%');
}, '>=', 10)->get();

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

内联关系存在查询

如果您想使用关系查询附加的单个简单 where 条件来查询关系是否存在,您可能会发现使用whereRelation,orWhereRelation,whereMorphRelation, 和orWhereMorphRelation 方法。例如,我们可能会查询所有具有未批准评论的帖子:

use App\Models\Post;

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

当然,就像调用查询生成器的where 方法,您还可以指定一个运算符:

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

查询关系缺席

检索模型记录时,您可能希望根据关系的缺失来限制结果。例如,假设您要检索所有博客文章don't 有什么意见。为此,您可以将关系的名称传递给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;

// Retrieve comments associated to posts or videos with a title like code%...
$comments = Comment::whereHasMorph(
    'commentable',
    [Post::class, Video::class],
    function (Builder $query) {
        $query->where('title', 'like', 'code%');
    }
)->get();

// Retrieve comments associated to posts with a title not like 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 声明,确保你打电话withCount 之后select 方法:

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

其他聚合函数

除了withCount 方法,Eloquent 提供withMin,withMax,withAvg,withSum, 和withExists 方法。这些方法将放置一个{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 模型定义了一个名为“morph to”的关系parentable 这允许我们检索父Photo 或者Post 给定模型ActivityFeed 实例。此外,我们假设Photo 模型“有很多”Tag 模型和Post 模型“有很多”Comment 楷模。

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

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 查询问题,考虑一个Book “属于”一个模型Author 模型:

<?php

namespace App\Models;

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

class Book extends Model
{
    /**
     * Get the author that wrote the book.
     */
    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
{
    /**
     * Get the parent of the activity feed record.
     */
    public function parentable(): MorphTo
    {
        return $this->morphTo();
    }
}

在这个例子中,我们假设Event,Photo, 和Post 模型可能会创建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
{
    /**
     * The relationships that should always be loaded.
     *
     * @var array
     */
    protected $with = ['author'];

    /**
     * Get the author that wrote the book.
     */
    public function author(): BelongsTo
    {
        return $this->belongsTo(Author::class);
    }

    /**
     * Get the genre of the book.
     */
    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;

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

在此示例中,Eloquent 只会在帖子所在的位置预先加载帖子title 列包含单词code.你可以打电话给其他人查询生成器 进一步自定义预加载操作的方法:

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

Warning
limittake 限制急切加载时可能不会使用查询生成器方法。

约束预加载morphTo 关系

如果你急于加载一个morphTo 关系,Eloquent 将运行多个查询来获取每种类型的相关模型。您可以使用MorphTo 关系的constrain 方法:

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

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

在这个例子中,Eloquent 只会急切加载没有被隐藏的帖子和有type “教育”的价值。

用关系存在约束渴望负载

有时您可能会发现自己需要检查关系是否存在,同时根据相同条件加载关系。例如,您可能希望只检索User 有孩子的模特Post 匹配给定查询条件的模型,同时也渴望加载匹配的帖子。您可以使用withWhereHas 方法:

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

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

延迟加载

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

use App\Models\Book;

$books = Book::all();

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

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

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

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

$book->loadMissing('author');

嵌套的延迟加载 &morphTo

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

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

<?php

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

class ActivityFeed extends Model
{
    /**
     * Get the parent of the activity feed record.
     */
    public function parentable(): MorphTo
    {
        return $this->morphTo();
    }
}

在这个例子中,我们假设Event,Photo, 和Post 模型可能会创建ActivityFeed 楷模。此外,我们假设Event 模型属于Calendar 模型,Photo 模型与Tag 模型,和Post 模型属于Author 模型。

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

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

防止延迟加载

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

preventLazyLoading 方法接受一个可选的布尔参数,指示是否应阻止延迟加载。例如,您可能希望只在非生产环境中禁用延迟加载,这样即使在生产代码中意外出现延迟加载关系,您的生产环境也能继续正常运行:

use Illuminate\Database\Eloquent\Model;

/**
 * Bootstrap any application services.
 */
public function boot(): void
{
    Model::preventLazyLoading(! $this->app->isProduction());
}

防止延迟加载后,Eloquent 会抛出一个Illuminate\Database\LazyLoadingViolationException当您的应用程序尝试延迟加载任何 Eloquent 关系时的异常。

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

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

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

插入和更新相关模型

save 方法

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

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

$post = Post::find(1);

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

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

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

$post->refresh();

// All comments, including the newly saved comment...
$post->comments;

递归保存模型和关系

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

$post = Post::find(1);

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

$post->push();

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

$post->pushQuietly();

create 方法

除了savesaveMany 方法,您也可以使用create 方法,它接受一个属性数组,创建一个模型,并将其插入到数据库中。和...之间的不同savecreate 就是它save 接受一个完整的 Eloquent 模型实例,同时create 接受普通的 PHParray.新创建的模型将由create 方法:

use App\Models\Post;

$post = Post::find(1);

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

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

$post = Post::find(1);

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

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

$user = User::find(1);

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

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

您也可以使用findOrNew,firstOrNew,firstOrCreate, 和updateOrCreate 方法创建和更新关系模型.

Note
在使用之前create 方法,一定要复习批量分配 文档。

属于关系

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

use App\Models\Account;

$account = Account::find(10);

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

$user->save();

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

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

$user->save();

多对多关系

安装/拆卸

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

use App\Models\User;

$user = User::find(1);

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

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

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

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

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

// Detach all roles from the user...
$user->roles()->detach();

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

$user = User::find(1);

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

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

同步关联

您也可以使用sync 构造多对多关联的方法。这sync 方法接受一组 ID 以放置在中间表上。任何不在给定数组中的 ID 都将从中间表中删除。因此,此操作完成后,中间表中将只存在给定数组中的 ID:

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

您还可以使用 ID 传递额外的中间表值:

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

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

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

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

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

切换协会

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

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

您还可以使用 ID 传递额外的中间表值:

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

更新中间表上的记录

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

$user = User::find(1);

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

触摸父时间戳

当一个模型定义一个belongsTo 或者belongsToMany 与另一个模型的关系,例如Comment 属于一个Post,有时在更新子模型时更新父模型的时间戳是有帮助的。

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

<?php

namespace App\Models;

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

class Comment extends Model
{
    /**
     * All of the relationships to be touched.
     *
     * @var array
     */
    protected $touches = ['post'];

    /**
     * Get the post that the comment belongs to.
     */
    public function post(): BelongsTo
    {
        return $this->belongsTo(Post::class);
    }
}

Warning
只有使用 Eloquent 更新子模型时,才会更新父模型时间戳save 方法。

豫ICP备18041297号-2