翻译进度
46
分块数量
4
参与人数

模型关联

这是一篇协同翻译的文章,你可以点击『我来翻译』按钮来参与翻译。


定义关联关系

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

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

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

一对一 / Has One

一对一关系是一种非常基础的数据库关系类型。 例如,一个 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');
dszhxb 翻译于 2周前

此外,Eloquent 默认假设外键的值应当与父模型主键列的值相匹配。换句话说,Eloquent 会在 Phone 记录的 user_id 列中,查找与用户 id 列相同的值。
如果你希望在关联中使用非 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 会尝试查找一个 User 模型,其 id 值与 Phone 模型中的 user_id 列相匹配。

Eloquent 会通过检查关联方法的名字,并在方法名后追加 _id,来推断外键的名称。
所以在本例中,Eloquent 默认假设 Phone 模型中存在 user_id 列。
但是,如果 Phone 模型中的外键并不是 user_id,你可以在 belongsTo 方法中传递第二个参数来自定义外键名称:

/**
 * 获取拥有该手机的用户。
 */
public function user(): BelongsTo
{
    return $this->belongsTo(User::class, 'foreign_key');
}
dszhxb 翻译于 2周前

如果父模型没有使用 id 作为主键,或者你希望通过其他列来查找关联模型,可以在 belongsTo 方法中传递第三个参数,用于指定父表的自定义键:

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

一对多 / Has Many

一对多关系用于定义这样的场景:一个模型作为父模型,可以拥有一个或多个子模型。
例如,一篇博客文章可以有无限多条评论。和其他 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 会将父模型的名称转换为蛇形命名「snake case」,并在其后加上 _id
因此,在本例中,Eloquent 假设 Comment 模型中的外键列为 post_id

当定义好关联方法后,我们就可以通过访问 comments 属性,来获取相关文章的 集合
需要记住的是,Eloquent 提供了「动态关联属性」,因此我们可以像访问模型属性一样访问关联方法:

use App\Models\Post;

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

foreach ($comments as $comment) {
    // ...
}
dszhxb 翻译于 2周前

由于所有关联也同时作为查询构建器使用,你可以通过调用 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;
    }
}

在上例中,即使每个 Post 模型的评论已被预加载,仍然会引入「N + 1」查询问题,因为 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();
dszhxb 翻译于 2周前

一对多(反向) / Belongs To

既然我们可以访问一篇文章的所有评论,现在我们来定义一个关联,让评论可以访问它的父文章。
要定义 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 会尝试查找一个 Post 模型,其 id 值与 Comment 模型中的 post_id 列相匹配。

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');
}
dszhxb 翻译于 2周前

默认模型

belongsTohasOnehasOneThroughmorphOne 关联允许你定义一个默认模型,当关联为 null 时返回该模型。
这种模式通常被称为空对象模式,可以帮助减少代码中的条件判断。
在下面的示例中,如果 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';
    });
}

查询 Belongs To 关联

在查询「belongs to」关联的子模型时,你可以手动构建 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();
dszhxb 翻译于 2周前

默认情况下,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');
}

[!注意]
由于 PostgreSQL 不支持对 UUID 列执行 MAX 函数,因此目前无法在 PostgreSQL UUID 列上使用 one-of-many 关系。

dszhxb 翻译于 2周前

将「多」关联转换为 Has One 关联

通常,当使用 latestOfManyoldestOfManyofMany 方法获取单个模型时,你可能已经为同一模型定义了「has many」关联。为了方便,Laravel 允许你通过在关联上调用 one 方法,将该关联轻松转换为「has one」关联:

/**
 * 获取用户的订单。
 */
public function orders(): HasMany
{
    return $this->hasMany(Order::class);
}

/**
 * 获取用户价格最高的订单。
 */
public function largestOrder(): HasOne
{
    return $this->orders()->one()->ofMany('price', 'max');
}

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

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

高级 Has One of Many 关联

有时可能需要构建更高级的「has one of many」关联。例如,Product 模型可能关联多个 Price 模型,即使在发布新价格后,这些价格仍会保留在系统中。此外,该产品的新价格数据可能会提前发布,通过 published_at 列在未来某个时间生效。

因此,总结来说,我们需要获取已发布的最新价格,且发布时间不晚于当前时间。另外,如果有两条价格的发布时间相同,我们会优先选择 ID 最大的价格。为实现这一点,我们必须向 ofMany 方法传递一个数组,其中包含用于确定最新价格的可排序列。此外,还需要向 ofMany 方法传递一个闭包作为第二个参数,该闭包将负责为关联查询添加额外的发布时间约束:

/**
 * 获取产品的当前价格。
 */
public function currentPricing(): HasOne
{
    return $this->hasOne(Price::class)->ofMany([
        'published_at' => 'max',
        'id' => 'max',
    ], function (Builder $query) {
        $query->where('published_at', '<', now());
    });
}
dszhxb 翻译于 2周前

远程一对一

「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

namespace App\Models;

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

class Mechanic extends Model
{
    /**
     * 获取汽车的车主。
     */
    public function carOwner(): HasOneThrough
    {
        return $this->hasOneThrough(Owner::class, Car::class);
    }
}

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

或者,如果在关联涉及的所有模型上都已经定义了相应的关系,你也可以通过调用 through 方法并提供这些关系的名称,来更简洁地定义「has-one-through」关联。例如,如果 Mechanic 模型有一个 cars 关联,而 Car 模型有一个 owner 关联,那么就可以这样定义一个连接技师和车主的「has-one-through」关联:

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

// 动态写法...
return $this->throughCars()->hasOwner();
dszhxb 翻译于 2周前

键约定

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

class Mechanic extends Model
{
    /**
     * 获取汽车的所有者。
     */
    public function carOwner(): HasOneThrough
    {
        return $this->hasOneThrough(
            Owner::class,
            Car::class,
            'mechanic_id', // cars 表上的外键...
            'car_id', // owners 表上的外键...
            'id', // mechanics 表上的本地键...
            'id' // cars 表上的本地键...
        );
    }
}

或者,如前所述,如果相关的关联已经在关系中涉及的所有模型上定义好,你可以通过调用 through 方法并提供这些关系的名称,流畅地定义一个「has-one-through」关联。这种方法的优点是可以复用现有关联上已经定义的键约定:

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

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

远程一对多

「has-many-through」关联提供了一种通过中间关联访问远程关联的便捷方式。例如,假设我们正在构建一个类似 Laravel Cloud 的部署平台。一个 Application 模型可能会通过一个中间的 Environment 模型访问多个 Deployment 模型。使用这个示例,你可以轻松获取给定应用的所有部署。让我们来看一下定义这个关联所需的表:

applications
    id - integer
    name - string

environments
    id - integer
    application_id - integer
    name - string

deployments
    id - integer
    environment_id - integer
    commit_hash - string
dszhxb 翻译于 2周前

现在我们已经了解了定义这种关联所需的表结构,接下来在 Application 模型中定义关联:

<?php

namespace App\Models;

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

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

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

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

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

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

尽管 Deployment 模型的表中没有 application_id 列,但通过 hasManyThrough 关联,仍可以通过 $application->deployments 访问应用的部署。要检索这些模型,Eloquent 会检查中间模型 Environment 表中的 application_id 列。找到相关的环境 ID 后,再用它们查询 Deployment 模型的表。

键约定

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

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

或者,如前所述,如果在关联涉及的所有模型上已经定义了相关关系,你可以通过调用 through 方法并提供这些关系的名称,流畅地定义一个「has-many-through」关联。这种方法的优点是可以复用现有关联上已经定义的键约定:

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

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

作用域关联

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

<?php

namespace App\Models;

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

class User extends Model
{
    /**
     * 获取用户的帖子。
     */
    public function posts(): HasMany
    {
        return $this->hasMany(Post::class)->latest();
    }

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

但是,如果你尝试通过 featuredPosts 方法创建模型,它的 featured 属性不会被设置为 true。如果你希望通过关联方法创建模型时,同时为所有通过该关联创建的模型指定默认属性,可以在构建关联查询时使用 withAttributes 方法:

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

withAttributes 方法会使用给定属性向查询添加 where 条件,并将这些属性添加到通过关联方法创建的任何模型上:

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

$post->featured; // true
dszhxb 翻译于 2周前

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

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

多对多关联

多对多关联比 hasOnehasMany 关联稍微复杂一些。一个多对多关联的示例是一个用户拥有多个角色,同时这些角色也可能被应用中的其他用户共享。例如,一个用户可能被分配「Author」和「Editor」角色;然而,这些角色也可能分配给其他用户。因此,一个用户有多个角色,一个角色有多个用户。

表结构

要定义这种关联,需要三个数据库表:usersrolesrole_userrole_user 表的名称来源于相关模型名称的字母顺序,并包含 user_idrole_id 列。该表用作连接用户和角色的中间表。

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

users
    id - integer
    name - string

roles
    id - integer
    name - string

role_user
    user_id - integer
    role_id - integer
dszhxb 翻译于 2周前

模型结构

多对多关联是通过定义一个方法并返回 belongsToMany 方法的结果来实现的。belongsToMany 方法由 Illuminate\Database\Eloquent\Model 基类提供,该基类被你应用中的所有 Eloquent 模型使用。例如,在 User 模型上定义一个 roles 方法。传递给该方法的第一个参数是相关模型类的名称:

<?php

namespace App\Models;

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

class User extends Model
{
    /**
     * 属于用户的角色。
     */
    public function roles(): BelongsToMany
    {
        return $this->belongsToMany(Role::class);
    }
}

定义关联后,你可以使用 roles 动态关联属性访问用户的角色:

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');
dszhxb 翻译于 2周前

定义关联的反向关系

要定义多对多关联的「反向关系」,你应在相关模型上定义一个方法,该方法也返回 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);
    }
}

如你所见,该关联的定义与 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');
dszhxb 翻译于 2周前

如果你希望中间表拥有由 Eloquent 自动维护的 created_atupdated_at 时间戳,可以在定义关联时调用 withTimestamps 方法:

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

[!警告]
使用 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');
dszhxb 翻译于 2周前

wherePivot 会向查询添加一个 where 条件约束,但在通过定义的关联创建新模型时不会添加指定的值。如果你需要在查询和创建关联时都使用特定的 pivot 值,可以使用 withPivotValue 方法:

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

通过中间表列排序查询

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

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

定义自定义中间表模型

如果你希望为多对多关联的中间表定义自定义模型,可以在定义关联时调用 using 方法。自定义 pivot 模型允许你在 pivot 模型上定义额外的行为,例如方法和类型转换。

自定义多对多 pivot 模型应继承 Illuminate\Database\Eloquent\Relations\Pivot 类,而自定义多态多对多 pivot 模型应继承 Illuminate\Database\Eloquent\Relations\MorphPivot 类。例如,我们可以定义一个 Role 模型,它使用自定义的 RoleUser pivot 模型:

<?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
{
    // ...
}

[!警告]
Pivot 模型不能使用 SoftDeletes trait。如果你需要对 pivot 记录进行软删除,请考虑将 pivot 模型转换为实际的 Eloquent 模型。

dszhxb 翻译于 2周前

自定义 Pivot 模型与自增 ID

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

/**
 * 指示 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

dszhxb 翻译于 2周前

模型结构

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

<?php

namespace App\Models;

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

class Image extends Model
{
    /**
     * 获取父级 imageable 模型(用户或文章)。
     */
    public function imageable(): MorphTo
    {
        return $this->morphTo();
    }
}

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

class Post extends Model
{
    /**
     * 获取文章的图片。
     */
    public function image(): MorphOne
    {
        return $this->morphOne(Image::class, 'imageable');
    }
}

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

class User extends Model
{
    /**
     * 获取用户的图片。
     */
    public function image(): MorphOne
    {
        return $this->morphOne(Image::class, 'imageable');
    }
}

获取关系

定义数据库表和模型后,你可以通过模型访问这些关系。例如,要获取文章的图片,可以访问 image 动态关系属性:

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');
}
dszhxb 翻译于 2周前

一对多(多态)

表结构

一对多多态关系类似于典型的一对多关系;但是,子模型可以通过单一关联属于多种类型的模型。例如,假设你的应用用户可以对文章和视频进行“评论”。使用多态关系,你可以使用单个 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
{
    /**
     * 获取父级 commentable 模型(文章或视频)。
     */
    public function commentable(): MorphTo
    {
        return $this->morphTo();
    }
}

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

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

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

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

获取关系

定义数据库表和模型后,你可以通过模型的动态关系属性访问这些关系。例如,要访问文章的所有评论,可以使用 comments 动态属性:

use App\Models\Post;

$post = Post::find(1);

foreach ($post->comments as $comment) {
    // ...
}
dszhxb 翻译于 2周前

你可以通过访问调用 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 自动将父模型填充到子模型上,可以在定义 morphMany 关系时调用 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();
dszhxb 翻译于 2周前

One of Many (Polymorphic)

Sometimes a model may have many related models, yet you want to easily retrieve the "latest" or "oldest" related model of the relationship. For example, a User model may be related to many Image models, but you want to define a convenient way to interact with the most recent image the user has uploaded. You may accomplish this using the morphOne relationship type combined with the ofMany methods:

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

Likewise, you may define a method to retrieve the "oldest", or first, related model of a relationship:

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

By default, the latestOfMany and oldestOfMany methods will retrieve the latest or oldest related model based on the model's primary key, which must be sortable. However, sometimes you may wish to retrieve a single model from a larger relationship using a different sorting criteria.

For example, using the ofMany method, you may retrieve the user's most "liked" image. The ofMany method accepts the sortable column as its first argument and which aggregate function (min or max) to apply when querying for the related model:

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

[!NOTE]
It is possible to construct more advanced "one of many" relationships. For more information, please consult the has one of many documentation.

Many to Many (Polymorphic)

Table Structure

Many-to-many polymorphic relations are slightly more complicated than "morph one" and "morph many" relationships. For example, a Post model and Video model could share a polymorphic relation to a Tag model. Using a many-to-many polymorphic relation in this situation would allow your application to have a single table of unique tags that may be associated with posts or videos. First, let's examine the table structure required to build this relationship:

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]
Before diving into polymorphic many-to-many relationships, you may benefit from reading the documentation on typical many-to-many relationships.

Model Structure

Next, we're ready to define the relationships on the models. The Post and Video models will both contain a tags method that calls the morphToMany method provided by the base Eloquent model class.

The morphToMany method accepts the name of the related model as well as the "relationship name". Based on the name we assigned to our intermediate table name and the keys it contains, we will refer to the relationship as "taggable":

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

Defining the Inverse of the Relationship

Next, on the Tag model, you should define a method for each of its possible parent models. So, in this example, we will define a posts method and a videos method. Both of these methods should return the result of the morphedByMany method.

The morphedByMany method accepts the name of the related model as well as the "relationship name". Based on the name we assigned to our intermediate table name and the keys it contains, we will refer to the relationship as "taggable":

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

Retrieving the Relationship

Once your database table and models are defined, you may access the relationships via your models. For example, to access all of the tags for a post, you may use the tags dynamic relationship property:

use App\Models\Post;

$post = Post::find(1);

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

You may retrieve the parent of a polymorphic relation from the polymorphic child model by accessing the name of the method that performs the call to morphedByMany. In this case, that is the posts or videos methods on the Tag model:

use App\Models\Tag;

$tag = Tag::find(1);

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

foreach ($tag->videos as $video) {
    // ...
}

Custom Polymorphic Types

By default, Laravel will use the fully qualified class name to store the "type" of the related model. For instance, given the one-to-many relationship example above where a Comment model may belong to a Post or a Video model, the default commentable_type would be either App\Models\Post or App\Models\Video, respectively. However, you may wish to decouple these values from your application's internal structure.

For example, instead of using the model names as the "type", we may use simple strings such as post and video. By doing so, the polymorphic "type" column values in our database will remain valid even if the models are renamed:

use Illuminate\Database\Eloquent\Relations\Relation;

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

You may call the enforceMorphMap method in the boot method of your App\Providers\AppServiceProvider class or create a separate service provider if you wish.

You may determine the morph alias of a given model at runtime using the model's getMorphClass method. Conversely, you may determine the fully-qualified class name associated with a morph alias using the Relation::getMorphedModel method:

use Illuminate\Database\Eloquent\Relations\Relation;

$alias = $post->getMorphClass();

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

[!WARNING]
When adding a "morph map" to your existing application, every morphable *_type column value in your database that still contains a fully-qualified class will need to be converted to its "map" name.

Dynamic Relationships

You may use the resolveRelationUsing method to define relations between Eloquent models at runtime. While not typically recommended for normal application development, this may occasionally be useful when developing Laravel packages.

The resolveRelationUsing method accepts the desired relationship name as its first argument. The second argument passed to the method should be a closure that accepts the model instance and returns a valid Eloquent relationship definition. Typically, you should configure dynamic relationships within the boot method of a service provider:

use App\Models\Order;
use App\Models\Customer;

Order::resolveRelationUsing('customer', function (Order $orderModel) {
    return $orderModel->belongsTo(Customer::class, 'customer_id');
});

[!WARNING]
When defining dynamic relationships, always provide explicit key name arguments to the Eloquent relationship methods.

Querying Relations

Since all Eloquent relationships are defined via methods, you may call those methods to obtain an instance of the relationship without actually executing a query to load the related models. In addition, all types of Eloquent relationships also serve as query builders, allowing you to continue to chain constraints onto the relationship query before finally executing the SQL query against your database.

For example, imagine a blog application in which a User model has many associated Post models:

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

You may query the posts relationship and add additional constraints to the relationship like so:

use App\Models\User;

$user = User::find(1);

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

You are able to use any of the Laravel query builder's methods on the relationship, so be sure to explore the query builder documentation to learn about all of the methods that are available to you.

Chaining orWhere Clauses After Relationships

As demonstrated in the example above, you are free to add additional constraints to relationships when querying them. However, use caution when chaining orWhere clauses onto a relationship, as the orWhere clauses will be logically grouped at the same level as the relationship constraint:

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

The example above will generate the following SQL. As you can see, the or clause instructs the query to return any post with greater than 100 votes. The query is no longer constrained to a specific user:

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

In most situations, you should use logical groups to group the conditional checks between parentheses:

use Illuminate\Database\Eloquent\Builder;

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

The example above will produce the following SQL. Note that the logical grouping has properly grouped the constraints and the query remains constrained to a specific user:

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

Relationship Methods vs. Dynamic Properties

If you do not need to add additional constraints to an Eloquent relationship query, you may access the relationship as if it were a property. For example, continuing to use our User and Post example models, we may access all of a user's posts like so:

use App\Models\User;

$user = User::find(1);

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

Dynamic relationship properties perform "lazy loading", meaning they will only load their relationship data when you actually access them. Because of this, developers often use eager loading to pre-load relationships they know will be accessed after loading the model. Eager loading provides a significant reduction in SQL queries that must be executed to load a model's relations.

Querying Relationship Existence

When retrieving model records, you may wish to limit your results based on the existence of a relationship. For example, imagine you want to retrieve all blog posts that have at least one comment. To do so, you may pass the name of the relationship to the has and orHas methods:

use App\Models\Post;

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

You may also specify an operator and count value to further customize the query:

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

Nested has statements may be constructed using "dot" notation. For example, you may retrieve all posts that have at least one comment that has at least one image:

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

If you need even more power, you may use the whereHas and orWhereHas methods to define additional query constraints on your has queries, such as inspecting the content of a comment:

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 does not currently support querying for relationship existence across databases. The relationships must exist within the same database.

Many to Many Relationship Existence Queries

The whereAttachedTo method may be used to query for models that have a many to many attachment to a model or collection of models:

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

You may also provide a collection instance to the whereAttachedTo method. When doing so, Laravel will retrieve models that are attached to any of the models within the collection:

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

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

Inline Relationship Existence Queries

If you would like to query for a relationship's existence with a single, simple where condition attached to the relationship query, you may find it more convenient to use the whereRelation, orWhereRelation, whereMorphRelation, and orWhereMorphRelation methods. For example, we may query for all posts that have unapproved comments:

use App\Models\Post;

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

Of course, like calls to the query builder's where method, you may also specify an operator:

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

Querying Relationship Absence

When retrieving model records, you may wish to limit your results based on the absence of a relationship. For example, imagine you want to retrieve all blog posts that don't have any comments. To do so, you may pass the name of the relationship to the doesntHave and orDoesntHave methods:

use App\Models\Post;

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

If you need even more power, you may use the whereDoesntHave and orWhereDoesntHave methods to add additional query constraints to your doesntHave queries, such as inspecting the content of a comment:

use Illuminate\Database\Eloquent\Builder;

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

You may use "dot" notation to execute a query against a nested relationship. For example, the following query will retrieve all posts that do not have comments as well as posts that have comments where none of the comments are from banned users:

use Illuminate\Database\Eloquent\Builder;

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

Querying Morph To Relationships

To query the existence of "morph to" relationships, you may use the whereHasMorph and whereDoesntHaveMorph methods. These methods accept the name of the relationship as their first argument. Next, the methods accept the names of the related models that you wish to include in the query. Finally, you may provide a closure which customizes the relationship query:

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

You may occasionally need to add query constraints based on the "type" of the related polymorphic model. The closure passed to the whereHasMorph method may receive a $type value as its second argument. This argument allows you to inspect the "type" of the query that is being built:

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

Sometimes you may want to query for the children of a "morph to" relationship's parent. You may accomplish this using the whereMorphedTo and whereNotMorphedTo methods, which will automatically determine the proper morph type mapping for the given model. These methods accept the name of the morphTo relationship as their first argument and the related parent model as their second argument:

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

Querying All Related Models

Instead of passing an array of possible polymorphic models, you may provide * as a wildcard value. This will instruct Laravel to retrieve all of the possible polymorphic types from the database. Laravel will execute an additional query in order to perform this operation:

use Illuminate\Database\Eloquent\Builder;

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

Aggregating Related Models

Counting Related Models

Sometimes you may want to count the number of related models for a given relationship without actually loading the models. To accomplish this, you may use the withCount method. The withCount method will place a {relation}_count attribute on the resulting models:

use App\Models\Post;

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

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

By passing an array to the withCount method, you may add the "counts" for multiple relations as well as add additional constraints to the queries:

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;

You may also alias the relationship count result, allowing multiple counts on the same relationship:

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;

Deferred Count Loading

Using the loadCount method, you may load a relationship count after the parent model has already been retrieved:

$book = Book::first();

$book->loadCount('genres');

If you need to set additional query constraints on the count query, you may pass an array keyed by the relationships you wish to count. The array values should be closures which receive the query builder instance:

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

Relationship Counting and Custom Select Statements

If you're combining withCount with a select statement, ensure that you call withCount after the select method:

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

Other Aggregate Functions

In addition to the withCount method, Eloquent provides withMin, withMax, withAvg, withSum, and withExists methods. These methods will place a {relation}_{function}_{column} attribute on your resulting models:

use App\Models\Post;

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

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

If you wish to access the result of the aggregate function using another name, you may specify your own alias:

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

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

Like the loadCount method, deferred versions of these methods are also available. These additional aggregate operations may be performed on Eloquent models that have already been retrieved:

$post = Post::first();

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

If you're combining these aggregate methods with a select statement, ensure that you call the aggregate methods after the select method:

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

Counting Related Models on Morph To Relationships

If you would like to eager load a "morph to" relationship, as well as related model counts for the various entities that may be returned by that relationship, you may utilize the with method in combination with the morphTo relationship's morphWithCount method.

In this example, let's assume that Photo and Post models may create ActivityFeed models. We will assume the ActivityFeed model defines a "morph to" relationship named parentable that allows us to retrieve the parent Photo or Post model for a given ActivityFeed instance. Additionally, let's assume that Photo models "have many" Tag models and Post models "have many" Comment models.

Now, let's imagine we want to retrieve ActivityFeed instances and eager load the parentable parent models for each ActivityFeed instance. In addition, we want to retrieve the number of tags that are associated with each parent photo and the number of comments that are associated with each parent post:

use Illuminate\Database\Eloquent\Relations\MorphTo;

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

Deferred Count Loading

Let's assume we have already retrieved a set of ActivityFeed models and now we would like to load the nested relationship counts for the various parentable models associated with the activity feeds. You may use the loadMorphCount method to accomplish this:

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

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

Eager Loading

When accessing Eloquent relationships as properties, the related models are "lazy loaded". This means the relationship data is not actually loaded until you first access the property. However, Eloquent can "eager load" relationships at the time you query the parent model. Eager loading alleviates the "N + 1" query problem. To illustrate the N + 1 query problem, consider a Book model that "belongs to" to an Author model:

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

Now, let's retrieve all books and their authors:

use App\Models\Book;

$books = Book::all();

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

This loop will execute one query to retrieve all of the books within the database table, then another query for each book in order to retrieve the book's author. So, if we have 25 books, the code above would run 26 queries: one for the original book, and 25 additional queries to retrieve the author of each book.

Thankfully, we can use eager loading to reduce this operation to just two queries. When building a query, you may specify which relationships should be eager loaded using the with method:

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

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

For this operation, only two queries will be executed - one query to retrieve all of the books and one query to retrieve all of the authors for all of the books:

select * from books

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

Eager Loading Multiple Relationships

Sometimes you may need to eager load several different relationships. To do so, just pass an array of relationships to the with method:

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

Nested Eager Loading

To eager load a relationship's relationships, you may use "dot" syntax. For example, let's eager load all of the book's authors and all of the author's personal contacts:

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

Alternatively, you may specify nested eager loaded relationships by providing a nested array to the with method, which can be convenient when eager loading multiple nested relationships:

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

Nested Eager Loading morphTo Relationships

If you would like to eager load a morphTo relationship, as well as nested relationships on the various entities that may be returned by that relationship, you may use the with method in combination with the morphTo relationship's morphWith method. To help illustrate this method, let's consider the following model:

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

In this example, let's assume Event, Photo, and Post models may create ActivityFeed models. Additionally, let's assume that Event models belong to a Calendar model, Photo models are associated with Tag models, and Post models belong to an Author model.

Using these model definitions and relationships, we may retrieve ActivityFeed model instances and eager load all parentable models and their respective nested relationships:

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

Eager Loading Specific Columns

You may not always need every column from the relationships you are retrieving. For this reason, Eloquent allows you to specify which columns of the relationship you would like to retrieve:

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

[!WARNING]
When using this feature, you should always include the id column and any relevant foreign key columns in the list of columns you wish to retrieve.

Eager Loading by Default

Sometimes you might want to always load some relationships when retrieving a model. To accomplish this, you may define a $with property on the model:

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

If you would like to remove an item from the $with property for a single query, you may use the without method:

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

If you would like to override all items within the $with property for a single query, you may use the withOnly method:

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

Constraining Eager Loads

Sometimes you may wish to eager load a relationship but also specify additional query conditions for the eager loading query. You can accomplish this by passing an array of relationships to the with method where the array key is a relationship name and the array value is a closure that adds additional constraints to the eager loading query:

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

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

In this example, Eloquent will only eager load posts where the post's title column contains the word code. You may call other query builder methods to further customize the eager loading operation:

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

Constraining Eager Loading of morphTo Relationships

If you are eager loading a morphTo relationship, Eloquent will run multiple queries to fetch each type of related model. You may add additional constraints to each of these queries using the MorphTo relation's constrain method:

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

In this example, Eloquent will only eager load posts that have not been hidden and videos that have a type value of "educational".

Constraining Eager Loads With Relationship Existence

You may sometimes find yourself needing to check for the existence of a relationship while simultaneously loading the relationship based on the same conditions. For example, you may wish to only retrieve User models that have child Post models matching a given query condition while also eager loading the matching posts. You may accomplish this using the withWhereHas method:

use App\Models\User;

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

Lazy Eager Loading

Sometimes you may need to eager load a relationship after the parent model has already been retrieved. For example, this may be useful if you need to dynamically decide whether to load related models:

use App\Models\Book;

$books = Book::all();

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

If you need to set additional query constraints on the eager loading query, you may pass an array keyed by the relationships you wish to load. The array values should be closure instances which receive the query instance:

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

To load a relationship only when it has not already been loaded, use the loadMissing method:

$book->loadMissing('author');

Nested Lazy Eager Loading and morphTo

If you would like to eager load a morphTo relationship, as well as nested relationships on the various entities that may be returned by that relationship, you may use the loadMorph method.

This method accepts the name of the morphTo relationship as its first argument, and an array of model / relationship pairs as its second argument. To help illustrate this method, let's consider the following model:

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

In this example, let's assume Event, Photo, and Post models may create ActivityFeed models. Additionally, let's assume that Event models belong to a Calendar model, Photo models are associated with Tag models, and Post models belong to an Author model.

Using these model definitions and relationships, we may retrieve ActivityFeed model instances and eager load all parentable models and their respective nested relationships:

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

Automatic Eager Loading

[!WARNING]
This feature is currently in beta in order to gather community feedback. The behavior and functionality of this feature may change even on patch releases.

In many cases, Laravel can automatically eager load the relationships you access. To enable automatic eager loading, you should invoke the Model::automaticallyEagerLoadRelationships method within the boot method of your application's AppServiceProvider:

use Illuminate\Database\Eloquent\Model;

/**
 * Bootstrap any application services.
 */
public function boot(): void
{
    Model::automaticallyEagerLoadRelationships();
}

When this feature is enabled, Laravel will attempt to automatically load any relationships you access that have not been previously loaded. For example, consider the following scenario:

use App\Models\User;

$users = User::all();

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

Typically, the code above would execute a query for each user in order to retrieve their posts, as well as a query for each post to retrieve its comments. However, when the automaticallyEagerLoadRelationships feature has been enabled, Laravel will automatically lazy eager load the posts for all users in the user collection when you attempt to access the posts on any of the retrieved users. Likewise, when you attempt to access the comments for any retrieved post, all comments will be lazy eager loaded for all posts that were originally retrieved.

If you do not want to globally enable automatic eager loading, you can still enable this feature for a single Eloquent collection instance by invoking the withRelationshipAutoloading method on the collection:

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

return $users->withRelationshipAutoloading();

Preventing Lazy Loading

As previously discussed, eager loading relationships can often provide significant performance benefits to your application. Therefore, if you would like, you may instruct Laravel to always prevent the lazy loading of relationships. To accomplish this, you may invoke the preventLazyLoading method offered by the base Eloquent model class. Typically, you should call this method within the boot method of your application's AppServiceProvider class.

The preventLazyLoading method accepts an optional boolean argument that indicates if lazy loading should be prevented. For example, you may wish to only disable lazy loading in non-production environments so that your production environment will continue to function normally even if a lazy loaded relationship is accidentally present in production code:

use Illuminate\Database\Eloquent\Model;

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

After preventing lazy loading, Eloquent will throw a Illuminate\Database\LazyLoadingViolationException exception when your application attempts to lazy load any Eloquent relationship.

You may customize the behavior of lazy loading violations using the handleLazyLoadingViolationsUsing method. For example, using this method, you may instruct lazy loading violations to only be logged instead of interrupting the application's execution with exceptions:

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

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

Inserting and Updating Related Models

The save Method

Eloquent provides convenient methods for adding new models to relationships. For example, perhaps you need to add a new comment to a post. Instead of manually setting the post_id attribute on the Comment model you may insert the comment using the relationship's save method:

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

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

$post = Post::find(1);

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

Note that we did not access the comments relationship as a dynamic property. Instead, we called the comments method to obtain an instance of the relationship. The save method will automatically add the appropriate post_id value to the new Comment model.

If you need to save multiple related models, you may use the saveMany method:

$post = Post::find(1);

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

The save and saveMany methods will persist the given model instances, but will not add the newly persisted models to any in-memory relationships that are already loaded onto the parent model. If you plan on accessing the relationship after using the save or saveMany methods, you may wish to use the refresh method to reload the model and its relationships:

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

$post->refresh();

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

Recursively Saving Models and Relationships

If you would like to save your model and all of its associated relationships, you may use the push method. In this example, the Post model will be saved as well as its comments and the comment's authors:

$post = Post::find(1);

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

$post->push();

The pushQuietly method may be used to save a model and its associated relationships without raising any events:

$post->pushQuietly();

The create Method

In addition to the save and saveMany methods, you may also use the create method, which accepts an array of attributes, creates a model, and inserts it into the database. The difference between save and create is that save accepts a full Eloquent model instance while create accepts a plain PHP array. The newly created model will be returned by the create method:

use App\Models\Post;

$post = Post::find(1);

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

You may use the createMany method to create multiple related models:

$post = Post::find(1);

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

The createQuietly and createManyQuietly methods may be used to create a model(s) without dispatching any events:

$user = User::find(1);

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

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

You may also use the findOrNew, firstOrNew, firstOrCreate, and updateOrCreate methods to create and update models on relationships.

[!NOTE]
Before using the create method, be sure to review the mass assignment documentation.

属于关系

如果想将子模型分配给新的父模型,可以使用 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 方法将从中间表中删除适当的记录;但是,两个模型将仍然保留在数据库中:

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

// 从用户中分离所有角色...
$user->roles()->detach();
dkp 翻译于 2周前

为了方便起见,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 当前已附加,它将被分离。同样,如果它当前处于分离状态,它将被附加:

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

你也可以传递附加表的额外中间值与 ID:

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

更新中间表上的记录

dkp 翻译于 2周前

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

$user = User::find(1);

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

更新父级时间戳

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

例如,当一个 Comment 模型被更新时,你可能希望自动「触摸」拥有的 Postupdated_at 时间戳,使其设置为当前日期和时间。为了实现这一点,你可以在子模型中添加一个 touches 属性,其中包含应在更新子模型时更新其 updated_at 时间戳的关系名称:

<?php

namespace App\Models;

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

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

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

警告
只有当子模型使用 Eloquent 的 save 方法进行更新时,父模型的时间戳才会被更新。

dkp 翻译于 2周前

本文章首发在 LearnKu.com 网站上。

本文中的所有译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接
我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。

《L02 从零构建论坛系统》
以构建论坛项目 LaraBBS 为线索,展开对 Laravel 框架的全面学习。应用程序架构思路贴近 Laravel 框架的设计哲学。
《G01 Go 实战入门》
从零开始带你一步步开发一个 Go 博客项目,让你在最短的时间内学会使用 Go 进行编码。项目结构很大程度上参考了 Laravel。
贡献者:4
讨论数量: 11
发起讨论 只看当前版本


kiyoma
whereHas 有缺陷,会把主表全都遍历一遍
3 个点赞 | 10 个回复 | 问答 | 课程版本 5.5
UpGod
自定义多态关联的类型字段 string 转 int 的问题?
2 个点赞 | 0 个回复 | 问答 | 课程版本 5.6
MoYuEr
hasOne? belongsTo? I am the Riddler!我是谜语人
1 个点赞 | 1 个回复 | 分享创造 | 课程版本 8.x
UpGod
一对一和一对多的多态关联怎么更新关系呢?
0 个点赞 | 9 个回复 | 问答 | 课程版本 9.x
SimonWang
一对一连表操作,返回数据希望是一维数组
0 个点赞 | 8 个回复 | 问答 | 课程版本 5.7
cnian
多态关联时如何指定查询的字段呢?
0 个点赞 | 3 个回复 | 问答 | 课程版本 7.x
模型ORM
0 个点赞 | 2 个回复 | 问答 | 课程版本 7.x
zf_cloud
Laravel 多态关联时 使用with取关联数据为null
0 个点赞 | 1 个回复 | 问答 | 课程版本 5.5