Skip to content
全部文档

Eloquent:关联

简介

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

定义关系

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

$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
{
    /**
     * Get the phone associated with the user.
     */
    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');

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

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

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

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

请记住,Eloquent将自动确定Comment模型的正确外键列。按照惯例,Eloquent将采用父模型的「蛇形」名称,并添加后缀_id。因此,在此示例中,Eloquent将假定Comment模型上的外键列是post_id

一旦定义了关系方法,我们就可以通过访问comments属性来访问相关注释的collection。请记住,由于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」查询问题:

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
{
    /**
     * Get the comments for the blog post.
     */
    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
{
    /**
     * 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模型,该模型的idComment模型上的post_id列相匹配。

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

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

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

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

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

默认型号

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

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

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

php
/**
 * 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();

你还可以向whereBelongsTo方法提供collection实例。执行此操作时,Laravel将检索属于集合中任何父模型的模型:

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

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

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

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

拥有众多之一

有时,一个模型可能有许多相关模型,但你希望轻松检索关系的「最新」或「最旧」相关模型。例如,User模型可能与许多Order模型相关,但你希望定义一种便捷的方式来与用户最近下的订单进行交互。你可以使用hasOne关系类型结合ofMany方法来完成此操作:

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

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

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

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

例如,使用 ofMany 方法可以检索用户最昂贵的订单。ofMany 方法的第一个参数是可排序列,第二个参数是查询关联模型时要应用的聚合函数(minmax):

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

WARNING

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

将「多」关系转换为只有一个关系

通常,当使用latestOfManyoldestOfManyofMany方法检索单个模型时,你已经为同一模型定义了「有多个」关系。为了方便起见,Laravel允许你通过在关系上调用one方法来轻松将此关系转换为「有一个」关系:

php
/**
 * Get the user's orders.
 */
public function orders(): HasMany
{
    return $this->hasMany(Order::class);
}

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

高级具有多种关系之一

可以构建更高级的「具有多个之一」关系。例如,Product模型可能有许多关联的Price模型,即使在发布新定价后,这些模型仍保留在系统中。此外,该产品的新定价数据可能可以通过published_at专栏提前发布,并在未来某个日期生效。

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

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

有一个通过

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

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

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
{
    /**
     * Get the car's owner.
     */
    public function carOwner(): HasOneThrough
    {
        return $this->hasOneThrough(Owner::class, Car::class);
    }
}

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

或者,如果已经在关系中涉及的所有模型上定义了相关关系,你可以通过调用through方法并提供这些关系的名称来流畅地定义「has-one-through」关系。例如,如果Mechanic模型具有cars关系,而Car模型具有owner关系,则你可以定义连接机械师和所有者的「has-one-through」关系,如下所示:

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

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

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

有很多通过

「has-many-through」关系提供了一种通过中间关系访问远程关系的便捷方法。例如,假设我们正在构建一个像Laravel Cloud这样的部署平台。Application模型可以通过中间Environment模型访问许多Deployment模型。使用此示例,你可以轻松收集给定应用程序的所有部署。让我们看一下定义这种关系所需的表:

projects
    id - integer
    name - string

environments
    id - integer
    project_id - integer
    name - string

deployments
    id - integer
    environment_id - integer
    commit_hash - string

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

php
<?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方法的第一个参数是我们希望访问的最终模型的名称,而第二个参数是中间模型的名称。

或者,如果已经在关系中涉及的所有模型上定义了相关关系,你可以通过调用through方法并提供这些关系的名称来流畅地定义「has-many-through」关系。例如,如果Application模型具有environments关系,Environment模型具有deployments关系,则你可以定义连接应用程序和部署的「has-many-through」关系,如下所示:

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

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

尽管 Deployment 模型的表不包含 project_id 列,但 hasManyThrough 关联可通过 $project->deployments 访问项目的部署记录。为检索这些模型,Eloquent 会检查中间 Environment 模型表上的 project_id 列。找到相关的环境 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方法并提供这些关系的名称来流畅地定义「has-many-through」关系。这种方法的优点是可以重用现有关系上已定义的关键约定:

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

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

范围关系

向模型添加额外的方法来约束关系是很常见的。例如,你可以将featuredPosts方法添加到User模型,该模型通过附加where约束来约束更广泛的posts关系:

php
<?php

namespace App\Models;

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

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

    /**
     * Get the user's featured posts.
     */
    public function featuredPosts(): HasMany
    {
        return $this->posts()->where('featured', true);
    }
}

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

php
/**
 * Get the user's featured posts.
 */
public function featuredPosts(): HasMany
{
    return $this->posts()->withAttributes(['featured' => true]);
}

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

$post = $user->featuredPosts()->create(['title' => 'Featured Post']);

$post->featured; // true

多对多关系

多对多关系比hasOnehasMany关系稍微复杂一些。多对多关系的一个示例是具有多个角色的用户,并且这些角色也由应用程序中的其他用户共享。例如,用户可能被分配「作者」和「编辑」的角色;但是,这些角色也可以分配给其他用户。因此,一个用户有多个角色,一个角色有多个用户。

表结构

要定义此关系,需要三个数据库表:usersrolesrole_userrole_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模型都使用该基类。例如,让我们在User模型上定义roles方法。传递给此方法的第一个参数是相关模型类的名称:

php
<?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方法的结果。为了完成我们的用户/角色示例,让我们在Role模型上定义users方法:

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

正如你所看到的,除了引用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');

wherePivot向查询添加了 where 子句约束,但在通过定义的关系创建新模型时不添加指定的值。如果你需要查询并创建与特定主值的关系,你可以使用withPivotValue方法:

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

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

你可以使用orderByPivotorderByPivotDesc方法对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
<?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
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Relations\Pivot;

class RoleUser extends Pivot
{
    // ...
}

WARNING

枢轴模型不能使用SoftDeletes特征。如果你需要软删除数据透视记录,请考虑将数据透视模型转换为实际的Eloquent模型。

自定义枢轴模型和递增 ID

若你定义了使用自定义中间表模型的多对多关联,且该中间表模型具有自增主键,则应确保自定义中间表模型类定义了值为 trueincrementing 属性。

php
/**
 * Indicates if the IDs are auto-incrementing.
 *
 * @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列将包含父模型的类名称。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
{
    /**
     * 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调用的方法的名称来检索多态模型的父级。在本例中,即Image模型上的imageable方法。因此,我们将访问该方法作为动态关系属性:

use App\Models\Image;

$image = Image::find(1);

$imageable = $image->imageable;

Image模型上的imageable关系将返回PostUser实例,具体取决于拥有图像的模型类型。

主要约定

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

php
/**
 * 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
<?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调用的方法的名称来检索多态子模型的父模型。在本例中,即Comment模型上的commentable方法。因此,我们将访问该方法作为动态关系属性,以便访问评论的父模型:

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

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

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

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

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

例如,使用 ofMany 方法可以检索用户最昂贵的订单。ofMany 方法的第一个参数是可排序列,第二个参数是查询关联模型时要应用的聚合函数(minmax):

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

INFO

可以构建更高级的「众多之一」关系。欲了解更多信息,请咨询has one of many documentation

多对多(多态)

表结构

多对多多态关联比「morph one」与「morph many」关联稍复杂一些。例如,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

INFO

在深入研究多态多对多关系之前,你可能会从阅读有关典型many-to-many relationships的文档中受益。

模型结构

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

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

php
<?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
<?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调用的方法的名称,从多态子模型中检索多态关系的父级。在本例中,即Tag模型上的postsvideos方法:

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,而不是使用模型名称作为「类型」。通过这样做,即使模型被重命名,我们数据库中的多态「类型」列值也将保持有效:

use Illuminate\Database\Eloquent\Relations\Relation;

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

你可以在App\Providers\AppServiceProvider类的boot方法中调用enforceMorphMap方法,或者根据需要创建单独的服务提供者。

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

use Illuminate\Database\Eloquent\Relations\Relation;

$alias = $post->getMorphClass();

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

WARNING

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

动态关系

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

resolveRelationUsing方法接受所需的关系名称作为其第一个参数。传递给该方法的第二个参数应该是一个接受模型实例并返回有效Eloquent关系定义的闭包。通常,你应该在service provider的引导方法中配置动态关系:

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关系也充当query builders,允许你在最终对数据库执行 SQL 查询之前继续将约束链接到关系查询。

例如,想象一个博客应用程序,其中User模型有许多关联的Post模型:

php
<?php

namespace App\Models;

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

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

你可以查询posts关系并向该关系添加其他约束,如下所示:

use App\Models\User;

$user = User::find(1);

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

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

在关系之后链接orWhere子句

如上面的示例所示,你可以在查询关系时自由地向关系添加其他约束。但是,将orWhere子句链接到关系时要小心,因为orWhere子句将在与关系约束相同的级别进行逻辑分组:

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

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

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

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

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示例模型,我们可以访问用户的所有帖子,如下所示:

use App\Models\User;

$user = User::find(1);

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

动态关系属性执行「延迟加载」,这意味着它们只会在你实际访问它们时加载它们的关系数据。因此,开发人员经常使用eager loading来预加载他们知道在加载模型后将访问的关系。预加载显着减少了加载模型关系时必须执行的 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 条件来查询关系是否存在,你可能会发现使用whereRelationorWhereRelationwhereMorphRelationorWhereMorphRelation方法更方便。例如,我们可以查询所有有未批准评论的帖子:

use App\Models\Post;

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

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

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

查询关系缺席

检索模型记录时,你可能希望根据不存在关系来限制结果。例如,假设你想要检索所有没有有任何评论的博客文章。为此,你可以将关系的名称传递给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();

有时你可能想要查询「变形为」关系的父级的子级。你可以使用whereMorphedTowhereNotMorphedTo方法来完成此操作,这些方法将自动确定给定模型的正确变形类型映射。这些方法接受morphTo关系的名称作为其第一个参数,并接受相关的父模型作为其第二个参数:

$comments = Comment::whereMorphedTo('commentable', $post)
    ->orWhereMorphedTo('commentable', $video)
    ->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
<?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;
}

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

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

在此示例中,我们假设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
<?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;
use Illuminate\Contracts\Database\Eloquent\Builder;

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

在此示例中,Eloquent将仅急切加载帖子的title列包含单词code的帖子。你可以调用其他query builder方法来进一步自定义急切加载操作:

$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值为「教育」的视频。

通过关系的存在来限制渴望的负载

有时你可能会发现自己需要检查关系是否存在,同时根据相同条件加载关系。例如,你可能希望仅检索具有与给定查询条件匹配的子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');
}

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

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

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

$book->loadMissing('author');

嵌套延迟预加载和morphTo

如果你想立即加载morphTo关系,以及该关系可能返回的各种实体上的嵌套关系,你可以使用loadMorph方法。

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

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

在此示例中,我们假设EventPhotoPost模型可能创建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始终防止关系的延迟加载。为此,你可以调用Eloquent模型基类提供的preventLazyLoading方法。通常,你应该在应用程序的AppServiceProvider类的boot方法中调用此方法。

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

php
use Illuminate\Database\Eloquent\Model;

/**
 * Bootstrap any application services.
 */
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属性:

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方法将保留给定的模型实例,但不会将新保留的模型添加到已加载到父模型上的任何内存中关系中。如果你计划在使用savesaveMany方法后访问关系,你可能希望使用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.'],
]);

你还可以使用findOrNewfirstOrNewfirstOrCreateupdateOrCreate方法来create and update models on relationships

INFO

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

属于关系

如果你想将子模型分配给新的父模型,可以使用associate方法。在此示例中,User模型定义了与Account模型的belongsTo关系。此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,
]);

触摸父时间戳

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

例如,当更新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
{
    /**
     * 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方法更新子模型时,才会更新父模型时间戳。