模型关联

未匹配的标注
本文档最新版为 10.x,旧版本可能放弃维护,推荐阅读最新版!

Eloquent: 关联

简介

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

定义关联

Eloquent 关联在 Eloquent 模型类中以方法的形式呈现。如同 Eloquent 模型本身,关联也可以作为强大的 查询语句构造器,使用,提供了强大的链式调用和查询功能。例如,我们可以在 posts 关联的链式调用中附加一个约束条件:

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

不过在深入使用关联之前,让我们先学习如何定义每种关联类型。

一对一

一对一是最基本的数据库关系。 例如,一个 User 模型可能与一个 Phone 模型相关联。为了定义这个关联关系,我们要在 User 模型中写一个 phone 方法, 在 phone 方法中调用 hasOne 方法并返回其结果。hasOne 方法被定义在 Illuminate\Database\Eloquent\Model 这个模型基类中:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

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

hasOne 方法的第一个参数是关联模型的类名。一旦定义了模型关联,我们就可以使用 Eloquent 的动态属性获得相关的记录。动态属性允许你访问该关联方法,就像访问模型中定义的属性一样:

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

Eloquent 基于父模型(User)的名称来确定关联模型(Phone)的外键名称。在本例中,会自动假定 Phone 模型有一个 user_id 的外键。如果你想重写这个约定,可以传递第二个参数给 hasOne 方法:

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

另外,Eloquent 假设外键的值是与父模型的主键(Primary Key)相同的。换句话说,Eloquent 将会通过 Phone 记录的 user_id 列中查找与用户表的 id 列相匹配的值。如果你希望使用自定义的主键值,而不是使用 id 或者模型中的 $primaryKey 属性,你可以给 hasOne 方法传递第三个参数:

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

定义反向关联

我们已经能从 User 模型访问到 Phone 模型了。接下来,让我们再在 Phone 模型上定义一个关联,它能让我们访问到拥有该电话的用户。我们可以使用 belongsTo 方法来定义反向关联, belongsTo 方法与 hasOne 方法相对应:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

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

在调用 user 方法时,Eloquent 会尝试查找一个 User 模型,该 User 模型上的 id 字段会与 Phone 模型上的 user_id 字段相匹配。

Eloquent 通过关联方法(user)的名称并使用 _id 作为后缀名来确定外键名称。因此,在本例中,Eloquent 会假设 Phone 模型有一个 user_id 字段。但是,如果 Phone 模型的外键不是 user_id,这时你可以给 belongsTo 方法的第二个参数传递一个自定义键名:

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

如果父模型的主键未使用 id 作为字段名,或者您想要使用其他的字段来匹配相关联的模型,那么您可以向 belongsTo 方法传递第三个参数,这个参数是在父模型中自己定义的字段名称:

/**
 * 获取当前手机号的用户
 */
public function user()
{
    return $this->belongsTo(User::class, 'foreign_key', 'owner_key');
}

一对多

当要定义一个模型是其他 (一个或者多个)模型的父模型这种关系时,可以使用一对多关联。例如,一篇博客可以有很多条评论。和其他模型关联一样,一对多关联也是在 Eloquent 模型文件中用一个方法来定义的:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Post extends Model
{
    /**
     * 获取这篇博客的所有评论
     */
    public function comments()
    {
        return $this->hasMany(Comment::class);
    }
}

注意,Eloquent 将会自动为 Comment 模型选择一个合适的外键。通常,这个外键是通过使用父模型的「蛇形命名」方式,然后再加上 _id. 的方式来命名的。因此,在上面这个例子中,Eloquent 将会默认 Comment 模型的外键是 post_id 字段。

如果关联方法被定义,那么我们就可以通过 comments 属性来访问相关的评论 集合。注意,由于 Eloquent 提供了「动态属性」,所以我们就可以像访问模型属性一样来访问关联方法:

use App\Models\Post;

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

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

由于所有的关系都可以看成是查询构造器,所以您也可以通过链式调用的方式,在 comments 方法中继续添加条件约束:

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

hasOne 方法一样,hasMany 方法中也可以接受额外的参数,从而来覆盖外键和本地键:

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

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

一对多 (反向) / 属于

目前我们可以访问一篇文章的所有评论,下面我们可以定义一个关联关系,从而让我们可以通过一条评论来获取到它所属的文章。这个关联关系是 hasMany 的反向,可以在子模型中通过 belongsTo 方法来定义这种关联关系:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Comment extends Model
{
    /**
     * 获取这条评论所属的文章。
     */
    public function post()
    {
        return $this->belongsTo(Post::class);
    }
}

如果定义了这种关联关系,那么我们就可以通过 Comment 模型中的 post 「动态属性」来获取到这条评论所属的文章:

use App\Models\Comment;

$comment = Comment::find(1);

return $comment->post->title;

在上面这个例子中,Eloquent 将会尝试寻找 Post 模型中的 id 字段与 Comment 模型中的 post_id 字段相匹配。

Eloquent 通过检查关联方法的名称,从而在关联方法名称后面加上 _ ,然后再加上父模型 (Post)的主键名称,以此来作为默认的外键名。因此,在上面这个例子中,Eloquent 将会默认 Post 模型在 comments 表中的外键是 post_id

但是,如果您的外键不遵循这种约定的话,那么您可以传递一个自定义的外键名来作为 belongsTo 方法的第二个参数:

/**
 * 获取这条评论所属的博客。
 */
public function post()
{
    return $this->belongsTo(Post::class, 'foreign_key');
}

如果你的主表(Post 表)不使用 id 来作为它的主键的话,或者你想通过其他列来关联相关模型的话,那么可以传递一个参数来作为 belongsTo 方法的第三个参数,这个参数是主表(Post 表)中想要作为关联关系的字段的名称。

/**
 * 获取这条评论所属的博客。
 */
public function post()
{
    return $this->belongsTo(Post::class, 'foreign_key', 'owner_key');
}

默认模型

belongsTohasOnehasOneThroughmorphOne 这些关联方法返回为 null 的时候,你可以定义一个默认的模型来返回。这种模式通常被称为 空对象模式,它可以帮你省略代码中的一些条件判断。在下面这个例子中,如果 Post 模型中没有用户,那么 user 关联关系将会返回一个空的 App\Models\User 模型:

/**
 * 获取这篇博客所属的用户。
 */
public function user()
{
    return $this->belongsTo(User::class)->withDefault();
}

如果想要这个默认模型中包含一些属性的话,可以向 withDefault 方法中传递一个数组或者一个闭包:

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

/**
 * 获取作者发布的博客。
 */
public function user()
{
    return $this->belongsTo(User::class)->withDefault(function ($user, $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();

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

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

一对多检索

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

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

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

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

latestOfManyoldestOfMany 默认根据模型的主键检索最新或最旧的相关记录,所以模型主键必须是 可排序 的。 但是,有时你可能希望使用不同的排序条件从更大的关系中检索单个模型。

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

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

注意:因为 PostgreSQL 不支持对 UUID 列执行 MAX 函数,所以目前不可能将多选之一关系与 PostgreSQL UUID 列结合使用。

进阶一对多检索

例如,一个「产品」模型可能有许多关联的「价格」模型,即使在新定价发布后,这些模型也会保留在系统中。此外,产品的新定价数据可能能够通过「published_at」列提前发布,以便在未来日期生效。

因此我们需要检索最新发布的定价,并且发布日期不能超过当前时间。 如果两个价格的发布日期相同,我们优先选择 ID 更大的价格。 为此,我们必须将一个数组传递给 ofMany 方法,其中包含确定最新价格的可排序列。为ofMany 方法的第二个参数传递一个闭包。 此闭包将负责为关系查询添加额外的发布日期约束:

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

远程一对一

「has-one-through」关联定义了一个模型和另外一个模型之间一对一的关系。同时,这种关联关系是需要第三个模型作为中间模型来实现的。

例如,在一个汽车维修的应用程序中,每一个 Mechanic 模型都与一个 Car 模型相关联,同时,每一个 Car 模型也和一个 Owner 模型相关联。虽然 mechaniowner 在数据库中并没有直接的关系,但是 mechani 可以通过 Car 模型来访问 owner。下面是定义这种关联关系所需要的数据表:

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;

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

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

键名约定

当使用远程一对一进行关联查询时,Eloquent 将会使用约定的外键名。如果你想要自定义相关联的键名的话,可以传递两个参数来作为「hasOneThrough」 方法的第三个和第四个参数。第三个参数是中间表的外键名。第四个参数是最终想要访问的模型的外键名。第五个参数是当前模型的本地键名,第六个参数是中间模型的本地键名:

class Mechanic extends Model
{
    /**
     * Get the car's owner.
     */
    public function carOwner()
    {
        return $this->hasOneThrough(
            Owner::class,
            Car::class,
            'mechanic_id', // 机械师表的外键...
            'car_id', // 车主表的外键...
            'id', // 机械师表的本地键...
            'id' // 汽车表的本地键...
        );
    }
}

远程一对多

「has-many-through」关联是可以通过中间关系来实现远程一对多的。例如,我们正在构建一个像 Laravel Vapor 这样的部署平台。一个 Project 模型可以通过一个中间的 Environment 模型来访问许多个 Deployment 模型。就像上面的这个例子,您可以在给定的 environment 中很方便的获取所有的 deployments。下面是定义这种关联关系所需要的数据表:

projects
    id - integer
    name - string

environments
    id - integer
    project_id - integer
    name - string

deployments
    id - integer
    environment_id - integer
    commit_hash - string

既然我们已经了解了远程一对多关联的表结构,那么就可以在 Project 模型中来定义这种关联关系:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

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

hasManyThrough 方法的第一个参数是我们最终想要访问的模型的名称,第二个参数是中间模型的名称。

虽然 Deployment 模型中没有 project_id 字段,但是在 hasManyThrough 中可以通过 $project->deployments来访问一个 project 的 deployments。如果想要查询上面的模型,Eloquent 将会检查在中间模型(Environment)中是否存在 project_id 字段。在找到相关的 Environment 的 id 后,就可以查询 Deployment 模型的内容了。

约定的键名

当使用远程一对多进行查询的时候,Eloquent 将会使用约定的外键名。如果您想要自定义相关联的键名的话,那么您可以传递两个参数来分别作为 hasManyThrough 方法的第三个和第四个参数。第三个参数是中间表的外键名。第四个参数是最终想要访问的模型的外键名。第五个参数是当前模型的本地键名,第六个参数是中间模型的本地键名:

class Project extends Model
{
    public function deployments()
    {
        return $this->hasManyThrough(
            Deployment::class,
            Environment::class,
            'project_id', // environments 表的外键名
            'environment_id', // deployments 表的外键名
            'id', // projects 表的本地键名
            'id' // environments 表的本地键名
        );
    }
}

多对多关联

多对多关联比 hasOnehasMany 关联稍微复杂些。举个例子,一个用户可以拥有多个角色,同时这些角色也可以分配给其他用户。例如,一个用户可是「作者」和「编辑」;当然,这些角色也可以分配给其他用户。所以,一个用户可以拥有多个角色,一个角色可以分配给多个用户。

表结构

要定义这种关联,需要三个数据库表: users, rolesrole_userrole_user 表的命名是由关联的两个模型按照字母顺序来的,并且包含了 user_idrole_id 字段。该表用作链接 usersroles 的中间表

特别提醒,由于角色可以属于多个用户,因此我们不能简单地在 roles 表上放置 user_id 列。如果这样,这意味着角色只能属于一个用户。为了支持将角色分配给多个用户,需要使用 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

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class User extends Model
{
    /**
     * 用户所拥有的角色
     */
    public function roles()
    {
        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

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Role extends Model
{
    /**
     * 拥有此角色的用户
     */
    public function users()
    {
        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 属性,它代表中间表的一个模型对象,并且可以像其他的 Eloquent 模型一样使用。

默认情况下,pivot 对象只包含两个关联模型的主键,如果你的中间表里还有其他额外字段,你必须在定义关联时明确指出:

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

如果你想让中间表自动维护 created_atupdated_at 时间戳,那么在定义关联时附加上 withTimestamps 方法即可:

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

注意:使用 Eloquent 自动维护时间戳的中间表需要同时具有 created_at 和 updated_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');

自定义中间表模型

如果你想定义一个自定义模型来表示多对多关系的中间表,你可以在定义关系时调用 using 方法。

自定义多对多中间表模型都必须继承 Illuminate\Database\Eloquent\Relations\Pivot 类,自定义多对多(多态)中间表模型必须继承 Illuminate\Database\Eloquent\Relations\MorphPivot 类。例如,我们在写 Role 模型的关联时,使用自定义中间表模型 RoleUser

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Role extends Model
{
    /**
     * 属于该角色的用户。
     */
    public function users()
    {
        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。 如果需要软删除数据关联记录,请考虑将数据关联模型转换为实际的 Eloquent 模型。

自定义中继模型和递增 ID

如果你用一个自定义的中继模型定义了多对多的关系,而且这个中继模型拥有一个自增的主键,你应当确保这个自定义中继模型类中定义了一个 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 时使用 imageable_type 列来判断父模型的「类型」。

模型结构

接下来,再看看建立关联的模型定义:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Image extends Model
{
    /**
     * 获取拥有此图片的模型
     */
    public function imageable()
    {
        return $this->morphTo();
    }
}

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

class User extends Model
{
    /**
     * 获取文章图片
     */
    public function image()
    {
        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 关系将返回 Post 实例或 User 实例,具体取决于模型拥有图像的类型。

自定义列

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

/**
 * 获取 image 实例所属的模型
 */
public function imageable()
{
    return $this->morphTo(__FUNCTION__, 'imageable_type', 'imageable_id');
}

一对多(多态)

表结构

一对多多态关联与简单的一对多关联类似,不过,目标模型可以在一个关联中从属于多个模型。假设应用中的用户可以同时「评论」文章和视频。使用多态关联,可以用单个 comments 表同时满足这些情况。我们还是先来看看用来构建这种关联的表结构:

posts
    id - integer
    title - string
    body - text

videos
    id - integer
    title - string
    url - string

comments
    id - integer
    body - text
    commentable_id - integer
    commentable_type - string

模型结构

接下来,看看构建这种关联的模型定义:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Comment extends Model
{
    /**
     * 获取拥有此评论的模型(Post 或 Video)
     */
    public function commentable()
    {
        return $this->morphTo();
    }
}

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

class Video extends Model
{
    /**
     * 获取此视频的所有评论
     */
    public function comments()
    {
        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 实例,其结果取决于评论所属的模型。

一对多检索(多态)

有时一个模型可能有许多相关模型,要检索关系的「最新」或「最旧」相关模型。 例如,一个 User 模型可能与许多 Image 模型相关,如果你想定义一种方便的方式来与用户上传的最新图像进行交互。 可以使用 morphOne 关系类型结合 ofMany 方法来完成此操作:

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

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

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

latestOfManyoldestOfMany 默认根据模型的主键检索最新或最旧的相关记录,所以模型主键必须是 可排序 的。 如果要使用不同的排序条件从更大的关系中检索单个模型。

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

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

技巧:要构建更高级的「一对多」关系。 请查看 进阶一对多检索.

多对多(多态)

表结构

多对多多态关联比 morphOnemorphMany 关联略微复杂一些。例如,博客 PostVideo 模型能够共享关联到 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

技巧:在深入研究多态多对多关系之前,阅读 多对多关系 的文档会对你有帮助。

模型结构

接下来,我们准备在模型上定义关联。Post 和 Video 模型都有调用 Eloquent 基类上 morphToMany 方法的 tags 方法。

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

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Post extends Model
{
    /**
     * 获取post下所有的标签.
     */
    public function tags()
    {
        return $this->morphToMany(Tag::class, 'taggable');
    }
}

定义多对多(多态)反向关系

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

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

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Tag extends Model
{
    /**
     * 获取分配给此标签的所有帖子。
     */
    public function posts()
    {
        return $this->morphedByMany(Post::class, 'taggable');
    }

    /**
     * 获取分配给此视频的所有帖子.
     */
    public function videos()
    {
        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 可能从属于一个 Post 或一个 Video,默认的 commentable_type 就将分别是 App\Post 或 App\Video。不过,你可能希望数据库与应用的内部结构解耦。在这种情况下,可以定义一个「morph 映射」来通知 Eloquent 使用自定义名称代替对应的类名:

例如,我们可以不使用模型名称作为「类型」,而是使用简单的字符串,例如 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);

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

动态关联

你可以使用 resolveRelationUsing 方法在运行时定义 Eloquent 模型之间的关系。虽然通常不建议在常规应用程序开发中使用它,但是在开发 Laravel 软件包时,这有时可能会很有用:

resolveRelationUsing 方法的第一个参数是关联名称。传递给该方法的第二个参数应该是一个闭包,闭包接受模型实例并返回一个有效的 Eloquent 关联定义。通常情况下,你应该在服务提供器的启动方法中配置动态关联。

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

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

注意:定义动态关系时,始终为 Eloquent 关系方法提供显式的键名参数。

查询关联

因为所有的 Eloquent 关联都是通过方法定义的,你可以调用这些方法来获取关联的实例,而无需真实执行查询来获取相关的模型。此外,所有的 Eloquent 关联也可以用作查询生成器,允许你在最终对数据库执行 SQL 查询之前,继续通过链式调用添加约束条件。

例如,假设有一个博客系统,它的 User 模型有许多关联的 Post 模型:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class User extends Model
{
    /**
     * 获取该用户的所有文章.
     */
    public function posts()
    {
        return $this->hasMany(Post::class);
    }
}

你可以查询 posts 关联,并给它添加额外的约束条件,如下例所示:

use App\Models\User;

$user = User::find(1);

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

你可以在关联上使用任意的 查询构造器 方法,所以一定要阅读查询构造器的文档,了解它的所有方法,这会对你非常有用。

在关联之后链式添加 orWhere 子句


如上例所示,你可以在查询关联时,自由的给关联添加额外的约束条件。但是,在将 orWhere 子句链接到关联上时,一定要小心,因为 orWhere 子句将在逻辑上与关联约束处于同一级别:

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

上面的例子将生成以下 SQL。像你看到的那样, 这个 or 子句的查询指令,将返回大于 100 票的任一用户,查询不再限于特定的用户:

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

在大多数情况下,你应该使用 逻辑组 在括号中对条件检查进行分组:

use Illuminate\Database\Eloquent\Builder;

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

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

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

关联方法 VS 动态属性

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

use App\Models\User;

$user = User::find(1);

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

动态属性是 「懒加载」 的,只有实际访问到才会加载关联数据。因此,通常用 预加载 来准备模型需要用到的关联数据。预加载能大量减少因加载模型关联执行的 SQL 语句。

查询已存在的关联

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

use App\Models\Post;

// 检索至少有一条评论的文章...
$posts = Post::has('comments')->get();

也可以指定运算符和数量来进一步自定义查询:

// 查出至少有三条评论的文章...
$posts = Post::has('comments', '>=', 3)->get();

也可以用「点」语法构造嵌套的 has 语句。例如,查出至少有一条评论和图片的文章:

// 查出至少有一条带图片的评论的文章...
$posts = Post::has('comments.images')->get();

如果需要更多功能,可以使用 whereHasorWhereHas 方法将「where」条件放到 has 查询上。这些方法允许你向关联加入自定义约束,比如检查评论内容:

use Illuminate\Database\Eloquent\Builder;

// 获取至少带有一条评论内容包含 code% 关键词的文章...
$posts = Post::whereHas('comments', function (Builder $query) {
    $query->where('content', 'like', 'code%');
})->get();

// 获取至少带有十条评论内容包含 code% 关键词的文章...
$posts = Post::whereHas('comments', function (Builder $query) {
    $query->where('content', 'like', 'code%');
}, '>=', 10)->get();

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

内联关系存在查询

如果你想使用附加到关系查询简单的 where 条件来确认关系是否存在,可以现使用 whereRelationwhereMorphRelation 。 例如,查询所有评论未获批准的帖子:

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 方法将「where」 条件加到 doesntHave 查询上。这些方法允许你向关联加入自定义限制,比如检测评论内容:

use Illuminate\Database\Eloquent\Builder;

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

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

use Illuminate\Database\Eloquent\Builder;

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

多态关联查询

要查询多态关联关系的存在,可以使用 whereHasMorphwhereDoesntHaveMorph 方法。这些方法接受关联名称作为它们的第一个参数。接下来,这些方法接受你想要包含在查询中的相关模型的名称。最后,也可以提供一个自定义关系查询的闭包:

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

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

// 检索与标题不类似代码的帖子相关的评论...
$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, $type) {
        $column = $type === Post::class ? 'content' : 'title';

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

查询所有关联模型

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

use Illuminate\Database\Eloquent\Builder;

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

聚合关联模型

关联模型计数

有时你可能需要计算给定关系的相关模型的数量,而不实际加载模型。 为此,你可以使用 withCount 方法。 withCount 方法将在生成的模型上放置 {relation}_count 属性:

use App\Models\Post;

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

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

通过将数组传递到 withCount 方法,可以为多个关系添加「计数」,并向查询添加附加约束:

use Illuminate\Database\Eloquent\Builder;

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

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

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

use Illuminate\Database\Eloquent\Builder;

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

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

延迟加载计数

使用 loadCount 可以在模型获取后加载关联关系的数量。

$book = Book::first();

$book->loadCount('genres');

如果你需要在统计时设置额外查询条件,可以通过传递键为关联关系名、值为查询闭包的数组来实现:

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

关联关系计数与自定义获取字段

如果你的查询同时包含 withCountselect,请确保 withCount 一定在 select 之后调用:

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

其他聚合函数

除了 withCount 方法外,Eloquent 还提供了 withMin, withMax, withAvgwithSum 等聚合方法。
这些方法会通过 {relation}_{function}_{column} 的命名方式将聚合结果添加到获取到的模型属性中:

use App\Models\Post;

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

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

如果你想使用其他名称访问聚合函数的结果,可以自定义的别名:

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

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

loadCount 方法类似,这些方法也有延迟调用的方法。这些延迟方法可在已获取到的 Eloquent 模型上调用:

$post = Post::first();

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

如果你将这些聚合方法和一个 select 语句组合在一起,确保你在 select 方法之后调用聚合方法:

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

计算多态关联关系的数量

如果你想预加载多态关联关系以及这个关联关系关联的其他关联关系的计数统计,可以通过将 with 方法与 morphTo 关系和 morphWithCount 方法结合来实现。

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

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

use Illuminate\Database\Eloquent\Relations\MorphTo;

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

延迟加载计数

假设我们已经检索了一组 ActivityFeed 模型,现在我们想要加载与活动提要关联的各种 parentable 模型的嵌套关系计数。 可以使用 loadMorphCount 方法来完成此操作:

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

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

预加载

当将 Eloquent 关系作为属性访问时,相关模型是延迟加载的。 这意味着在你第一次访问该属性之前不会实际加载关联数据。 但是,Eloquent 可以在查询父模型时主动加载关联关系。 预加载减轻了 N + 1 查询问题。 为了说明 N + 1 查询问题,请参考属于 Author 模型的 Book 模型:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Book extends Model
{
    /**
     * 获取作者的所有书籍
     */
    public function author()
    {
        return $this->belongsTo(Author::class);
    }
}

我们检索所有书籍及其作者:

use App\Models\Book;

$books = Book::all();

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

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

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

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

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

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

select * from books

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

预加载多个关联

有时,你可能需要在单一操作中预加载几个不同的关联。要达成此目的,只要向 with 方法传递多个关联名称构成的数组参数:

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

嵌套预加载

可以使用 「点」 语法预加载嵌套关联。比如在一个 Eloquent 语句中预加载所有书籍作者及其联系方式:

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

嵌套预加载 morphTo 关联

如果你希望加载一个 morphTo 关系,以及该关系可能返回的各种实体的嵌套关系,可以将 with 方法与 morphTo 关系的 morphWith 方法结合使用。为了说明这种方法,让我们参考以下模型:

<?php

use Illuminate\Database\Eloquent\Model;

class ActivityFeed extends Model
{
    /**
     * 获取活动提要记录的父级
     */
    public function parentable()
    {
        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();

注意:使用此功能时,应始终在要检索的列列表中包括 id 列和任何相关的外键列。

默认预加载

有时可能希望在查询模型时始终加载某些关联。 为此,你可以在模型上定义 $with 属性

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Book extends Model
{
    /**
     * 默认加载的关联
     *
     * @var array
     */
    protected $with = ['author'];

    /**
     * 获取书籍作者
     */
    public function author()
    {
        return $this->belongsTo(Author::class);
    }

    /**
     * 获取书籍类型.
     */
    public function genre()
    {
        return $this->belongsTo(Genre::class);
    }
}

如果你想从单个查询的 $with 属性中删除一个预加载,你可以使用 without 方法:

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

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

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

为预加载添加约束

有时,你可能希望预加载一个关联,同时为预加载查询添加额外查询条件。您可以通过将一个关联数组传递给 with 方法来实现这一点,其中数组键是关联名称,数组值是一个闭包,它为预先加载查询添加了额外的约束:

use App\Models\User;

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

在这个例子中,Eloquent 只会预加载帖子的 title 列包含单词 code 的帖子。 你可以调用其他 查询构造器 方法来自定义预加载操作:

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

注意:T在约束预加载时,不能使用 limit 和 take 查询构造器方法。

morphTo 关联预加载添加约束

预加载 morphTo 关联关系时,Eloquent 将运行多个查询来获取每种类型的相关模型。 您可以使用 MorphTo 关系的 constrain 方法为每个查询添加额外的约束:

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

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

在这个例子中,Eloquent 只会预先加载未被隐藏的帖子,并且视频的 type 值为 educational

延迟预加载

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

use App\Models\Book;

$books = Book::all();

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

如果要在渴求式加载的查询语句中进行条件约束,可以通过数组的形式去加载,键为对应的关联关系,值为 Closure 闭包函数,该闭包的参数为一个查询实例:

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

如果希望仅加载未被加载的关联关系时,你可以使用 loadMissing 方法:

$book->loadMissing('author');

嵌套延迟预加载 & morphTo

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

这个方法接受 morphTo 关系的名称作为它的第一个参数,第二个参数接收模型数组、关系数组。例子:

<?php

use Illuminate\Database\Eloquent\Model;

class ActivityFeed extends Model
{
    /**
     * 获取活动提要记录的父项。
     */
    public function parentable()
    {
        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 方法接受一个可选的布尔类型的参数,表示是否阻止延迟加载。例如,你可能希望只在非生产环境中禁用延迟加载,这样即使在生产环境代码中意外出现了延迟加载关系,你的生产环境也能继续正常运行。

use Illuminate\Database\Eloquent\Model;

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

在阻止延迟加载之后,当你的应用程序尝试延迟加载任何 Eloquent 关系时,Eloquent 将抛出Illuminate\Database\LazyLoadingViolationException异常。

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

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

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

插入 & 更新关联模型

save 方法

Eloquent 提供了向关系中添加新模型的便捷方法。例如,你可能需要向一篇文章(Post 模型)添加一条新的评论(Comment 模型),你不用手动设置 Comment 模型上的 post_id 属性,你可以直接使用关联模型中的 save 方法:

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

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

$post = Post::find(1);

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

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

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

$post = Post::find(1);

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

savesaveMany 方法不会将新模型(Comment)加载到父模型(Post) 上, 如果你计划在使用 savesaveMany 方法后访问该关联模型(Comment),你需要使用 refresh 方法重新加载模型及其关联,这样你就可以访问到所有评论,包括新保存的评论了:

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

$post->refresh();

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

递归保存模型和关联数据

如果你想 save 模型及其所有关联数据,你可以使用 push 方法,在此示例中,将保存 Post 模型及其评论和评论作者:

$post = Post::find(1);

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

$post->push();

新增方法

除了 savesaveMany 方法外,你还可以使用 create 方法。它接受一个属性数组,同时会创建模型并插入到数据库中。 还有, savecreate 方法的不同之处在于, save 方法接受一个完整的 Eloquent 模型实例,而 create 则接受普通的 PHP 数组:

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.'],
]);

你还可以使用 findOrNew, firstOrNew, firstOrCreate, 和 updateOrCreate 方法来 创建和更新关系模型

技巧:在使用 create 方法前,请务必确保查看过本文档的 批量赋值 章节。

Belongs To 关联

如果您想将子模型分配给新的父模型,您可以使用 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();

为了方便起见,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 已被附加在中间表中,那么它将会被移除,同样,如果给定的 ID 已被移除,它将会被附加:

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

更新中间表上的记录

如果你需要在中间表中更新一条已存在的记录,可以使用 updateExistingPivot 方法。 此方法接收中间表的外键与要更新的数据数组进行更新:

$user = User::find(1);

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

更新父级时间戳

当一个模型属 belongsTo 或者 belongsToMany 另一个模型时, 例如 Comment 属于 Post ,有时更新子模型导致更新父模型时间戳非常有用。

例如,当 Comment 模型被更新时,你需要自动「触发」父级 Post 模型的 updated_at 时间戳的更新。Eloquent 让它变得简单。只要在子模型加一个包含关联名称的 touches 属性即可:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

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

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

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

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

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

原文地址:https://learnku.com/docs/laravel/9.x/elo...

译文地址:https://learnku.com/docs/laravel/9.x/elo...

上一篇 下一篇
《L05 电商实战》
从零开发一个电商项目,功能包括电商后台、商品 & SKU 管理、购物车、订单管理、支付宝支付、微信支付、订单退款流程、优惠券等
《L04 微信小程序从零到发布》
从小程序个人账户申请开始,带你一步步进行开发一个微信小程序,直到提交微信控制台上线发布。
贡献者:22
讨论数量: 22
发起讨论 只看当前版本


kiyoma
whereHas 有缺陷,会把主表全都遍历一遍
3 个点赞 | 10 个回复 | 问答 | 课程版本 5.5
UpGod
自定义多态关联的类型字段 string 转 int 的问题?
2 个点赞 | 0 个回复 | 问答 | 课程版本 5.6
oneSmile
怎么获取模型所关联的模型?
1 个点赞 | 2 个回复 | 问答 | 课程版本 5.5
MoYuEr
hasOne? belongsTo? I am the Riddler!我是谜语人
1 个点赞 | 1 个回复 | 分享创造 | 课程版本 8.x
UpGod
多态关联如何更新父模型的时间戳?
0 个点赞 | 15 个回复 | 问答 | 课程版本 5.5
UpGod
一对一和一对多的多态关联怎么更新关系呢?
0 个点赞 | 9 个回复 | 问答 | 课程版本 9.x
SimonWang
一对一连表操作,返回数据希望是一维数组
0 个点赞 | 8 个回复 | 问答 | 课程版本 5.7
drinke9
Laravel 模型关联不同数据库之间能否使用关联模型?
0 个点赞 | 7 个回复 | 问答 | 课程版本 5.6
cnian
多态关联时如何指定查询的字段呢?
0 个点赞 | 3 个回复 | 问答 | 课程版本 7.x
xiwang6428
模型关联是否要在数据库表中建立实际关联?
0 个点赞 | 3 个回复 | 问答 | 课程版本 5.5
模型ORM
0 个点赞 | 2 个回复 | 问答 | 课程版本 7.x
Adolph-Wu
多对多模型,求帮助???
0 个点赞 | 2 个回复 | 问答 | 课程版本 5.6
lvan
是否一定需要在 MySQL 创建了主外键才能使用关联模型?
0 个点赞 | 2 个回复 | 问答 | 课程版本 5.6
licxisky
ruodee
字都认识,但还是懵懵懂懂?
0 个点赞 | 2 个回复 | 问答 | 课程版本 5.6
zf_cloud
Laravel 多态关联时 使用with取关联数据为null
0 个点赞 | 1 个回复 | 问答 | 课程版本 5.5
阿伦
多对多关联的模型,如何进行条件查询?
0 个点赞 | 1 个回复 | 问答 | 课程版本 5.6
tsin
文档中的更新父级时间戳方法不起作用
0 个点赞 | 1 个回复 | 问答 | 课程版本 5.6
clogcabin
复合主键的关联更新,有好的办法不
0 个点赞 | 0 个回复 | 问答 | 课程版本 5.7