Skip to content

Eloquent: 关系

介绍

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

定义关系

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

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

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

exclamation

关系名称不能与属性名称冲突,因为这可能导致模型无法知道要解析哪个。

一对一

一对一关系是非常基本的关系。例如,一个 User 模型可能与一个 Phone 相关联。要定义这种关系,我们在 User 模型上放置一个 phone 方法。phone 方法应调用 hasOne 方法并返回其结果:

php
<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

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

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

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

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

php
return $this->hasOne('App\Phone', 'foreign_key');

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

php
return $this->hasOne('App\Phone', 'foreign_key', 'local_key');

定义关系的反向

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

php
<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

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

在上面的示例中,Eloquent 将尝试将 Phone 模型中的 user_idUser 模型中的 id 匹配。Eloquent 通过检查关系方法的名称并在方法名称后加上 _id 来确定默认的外键名称。但是,如果 Phone 模型上的外键不是 user_id,您可以将自定义键名作为第二个参数传递给 belongsTo 方法:

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

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

php
/**
 * 获取拥有电话的用户。
 */
public function user()
{
    return $this->belongsTo('App\User', 'foreign_key', 'other_key');
}

一对多

一对多关系用于定义一个模型拥有任意数量其他模型的关系。例如,一篇博客文章可能有无限数量的评论。像所有其他 Eloquent 关系一样,一对多关系通过在 Eloquent 模型上放置一个函数来定义:

php
<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Post extends Model
{
    /**
     * 获取博客文章的评论。
     */
    public function comments()
    {
        return $this->hasMany('App\Comment');
    }
}

请记住,Eloquent 将自动确定 Comment 模型上的正确外键列。按照惯例,Eloquent 将采用拥有模型的“蛇形命名”并在其后加上 _id。因此,对于此示例,Eloquent 将假定 Comment 模型上的外键是 post_id

一旦定义了关系,我们可以通过访问 comments 属性来访问评论集合。请记住,由于 Eloquent 提供了“动态属性”,我们可以像访问模型上定义的属性一样访问关系方法:

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

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

由于所有关系也作为查询构建器,您可以通过调用 comments 方法并继续在查询上链式添加条件来添加进一步的约束以检索哪些评论:

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

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

php
return $this->hasMany('App\Comment', 'foreign_key');

return $this->hasMany('App\Comment', 'foreign_key', 'local_key');

一对多(反向)

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

php
<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Comment extends Model
{
    /**
     * 获取拥有评论的文章。
     */
    public function post()
    {
        return $this->belongsTo('App\Post');
    }
}

一旦定义了关系,我们可以通过访问 post “动态属性”来检索 CommentPost 模型:

php
$comment = App\Comment::find(1);

echo $comment->post->title;

在上面的示例中,Eloquent 将尝试将 Comment 模型中的 post_idPost 模型中的 id 匹配。Eloquent 通过检查关系方法的名称并在方法名称后加上 _ 和主键列的名称来确定默认的外键名称。但是,如果 Comment 模型上的外键不是 post_id,您可以将自定义键名作为第二个参数传递给 belongsTo 方法:

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

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

php
/**
 * 获取拥有评论的文章。
 */
public function post()
{
    return $this->belongsTo('App\Post', 'foreign_key', 'other_key');
}

多对多

多对多关系比 hasOnehasMany 关系稍微复杂一些。这样的关系的一个例子是一个用户有许多角色,而这些角色也被其他用户共享。例如,许多用户可能具有“管理员”角色。

表结构

要定义这种关系,需要三个数据库表:usersrolesrole_userrole_user 表是从相关模型名称的字母顺序派生的,并包含 user_idrole_id 列:

php
users
    id - integer
    name - string

roles
    id - integer
    name - string

role_user
    user_id - integer
    role_id - integer

模型结构

多对多关系通过编写一个返回 belongsToMany 方法结果的方法来定义。例如,让我们在 User 模型上定义 roles 方法:

php
<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

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

一旦定义了关系,您可以使用 roles 动态属性访问用户的角色:

php
$user = App\User::find(1);

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

像所有其他关系类型一样,您可以调用 roles 方法以继续在关系上链式添加查询约束:

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

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

php
return $this->belongsToMany('App\Role', 'role_user');

除了自定义连接表的名称,您还可以通过传递额外的参数给 belongsToMany 方法来自定义表上键的列名。第三个参数是您定义关系的模型的外键名称,而第四个参数是您要连接的模型的外键名称:

php
return $this->belongsToMany('App\Role', 'role_user', 'user_id', 'role_id');

定义关系的反向

要定义多对多关系的反向,您可以在相关模型上放置另一个 belongsToMany 调用。为了继续我们的用户角色示例,让我们在 Role 模型上定义 users 方法:

php
<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

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

如您所见,关系的定义与其 User 对应关系完全相同,除了引用 App\User 模型。由于我们重用了 belongsToMany 方法,因此在定义多对多关系的反向时,所有常见的表和键自定义选项都可用。

检索中间表列

正如您已经了解到的,处理多对多关系需要中间表的存在。Eloquent 提供了一些非常有用的方式来与此表进行交互。例如,假设我们的 User 对象有许多 Role 对象与之相关。在访问此关系后,我们可以使用模型上的 pivot 属性访问中间表:

php
$user = App\User::find(1);

foreach ($user->roles as $role) {
    echo $role->pivot->created_at;
}

请注意,我们检索的每个 Role 模型都自动分配了一个 pivot 属性。此属性包含一个表示中间表的模型,并可以像任何其他 Eloquent 模型一样使用。

默认情况下,只有模型键会出现在 pivot 对象上。如果您的枢纽表包含额外的属性,您必须在定义关系时指定它们:

php
return $this->belongsToMany('App\Role')->withPivot('column1', 'column2');

如果您希望您的枢纽表具有自动维护的 created_atupdated_at 时间戳,请在关系定义中使用 withTimestamps 方法:

php
return $this->belongsToMany('App\Role')->withTimestamps();

自定义 pivot 属性名称

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

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

php
return $this->belongsToMany('App\Podcast')
                ->as('subscription')
                ->withTimestamps();

完成此操作后,您可以使用自定义名称访问中间表数据:

php
$users = User::with('podcasts')->get();

foreach ($users->flatMap->podcasts as $podcast) {
    echo $podcast->subscription->created_at;
}

通过中间表列过滤关系

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

php
return $this->belongsToMany('App\Role')->wherePivot('approved', 1);

return $this->belongsToMany('App\Role')->wherePivotIn('priority', [1, 2]);

return $this->belongsToMany('App\Role')->wherePivotNotIn('priority', [1, 2]);

定义自定义中间表模型

如果您希望定义一个自定义模型来表示关系的中间表,可以在定义关系时调用 using 方法。自定义多对多枢纽模型应扩展 Illuminate\Database\Eloquent\Relations\Pivot 类,而自定义多态多对多枢纽模型应扩展 Illuminate\Database\Eloquent\Relations\MorphPivot 类。例如,我们可以定义一个使用自定义 RoleUser 枢纽模型的 Role

php
<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Role extends Model
{
    /**
     * 属于角色的用户。
     */
    public function users()
    {
        return $this->belongsToMany('App\User')->using('App\RoleUser');
    }
}

在定义 RoleUser 模型时,我们将扩展 Pivot 类:

php
<?php

namespace App;

use Illuminate\Database\Eloquent\Relations\Pivot;

class RoleUser extends Pivot
{
    //
}

您可以结合使用 usingwithPivot 来检索中间表的列。例如,您可以通过将列名传递给 withPivot 方法来从 RoleUser 枢纽表中检索 created_byupdated_by 列:

php
<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Role extends Model
{
    /**
     * 属于角色的用户。
     */
    public function users()
    {
        return $this->belongsToMany('App\User')
                        ->using('App\RoleUser')
                        ->withPivot([
                            'created_by',
                            'updated_by',
                        ]);
    }
}
lightbulb

枢纽模型可能不使用 SoftDeletes trait。如果您需要软删除枢纽记录,请考虑将您的枢纽模型转换为实际的 Eloquent 模型。

自定义枢纽模型和自增 ID

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

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

一对一穿透

“一对一穿透”关系通过单个中间关系链接模型。

例如,在一个汽车修理店应用程序中,每个 Mechanic 可能有一辆 Car,每辆 Car 可能有一个 Owner。虽然 MechanicOwner 没有直接连接,但 Mechanic 可以通过 Car 本身访问 Owner。让我们看看定义此关系所需的表:

php
mechanics
    id - integer
    name - string

cars
    id - integer
    model - string
    mechanic_id - integer

owners
    id - integer
    name - string
    car_id - integer

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

php
<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Mechanic extends Model
{
    /**
     * 获取汽车的所有者。
     */
    public function carOwner()
    {
        return $this->hasOneThrough('App\Owner', 'App\Car');
    }
}

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

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

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

一对多穿透

“一对多穿透”关系通过中间关系提供了访问远程关系的便捷捷径。例如,一个 Country 模型可能通过中间 User 模型拥有许多 Post 模型。在此示例中,您可以轻松地收集给定国家的所有博客文章。让我们看看定义此关系所需的表:

php
countries
    id - integer
    name - string

users
    id - integer
    country_id - integer
    name - string

posts
    id - integer
    user_id - integer
    title - string

虽然 posts 不包含 country_id 列,但 hasManyThrough 关系通过 $country->posts 提供对国家的文章的访问。为了执行此查询,Eloquent 检查中间 users 表上的 country_id。在找到匹配的用户 ID 后,它们用于查询 posts 表。

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

php
<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Country extends Model
{
    /**
     * 获取国家的所有文章。
     */
    public function posts()
    {
        return $this->hasManyThrough('App\Post', 'App\User');
    }
}

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

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

php
class Country extends Model
{
    public function posts()
    {
        return $this->hasManyThrough(
            'App\Post',
            'App\User',
            'country_id', // users 表上的外键...
            'user_id', // posts 表上的外键...
            'id', // countries 表上的本地键...
            'id' // users 表上的本地键...
        );
    }
}

多态关系

多态关系允许目标模型使用单个关联属于多个类型的模型。

一对一(多态)

表结构

一对一多态关系类似于简单的一对一关系;然而,目标模型可以在单个关联上属于多个类型的模型。例如,博客 PostUser 可能与 Image 模型共享多态关系。使用一对一多态关系允许您拥有一个用于博客文章和用户帐户的唯一图像列表。首先,让我们检查表结构:

php
posts
    id - integer
    name - string

users
    id - integer
    name - string

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

注意 images 表上的 imageable_idimageable_type 列。imageable_id 列将包含文章或用户的 ID 值,而 imageable_type 列将包含父模型的类名。imageable_type 列由 Eloquent 用于确定访问 imageable 关系时要返回的父模型的“类型”。

模型结构

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

php
<?php

namespace App;

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('App\Image', 'imageable');
    }
}

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

检索关系

一旦定义了数据库表和模型,您可以通过模型访问关系。例如,要检索文章的图像,我们可以使用 image 动态属性:

php
$post = App\Post::find(1);

$image = $post->image;

您还可以通过访问执行 morphTo 调用的方法名称从多态模型中检索父级。在我们的例子中,这是 Image 模型上的 imageable 方法。因此,我们将访问该方法作为动态属性:

php
$image = App\Image::find(1);

$imageable = $image->imageable;

Image 模型上的 imageable 关系将返回 PostUser 实例,具体取决于哪个类型的模型拥有图像。如果您需要为 morphTo 关系指定自定义 typeid 列,请确保始终将关系名称(应与方法名称完全匹配)作为第一个参数传递:

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

一对多(多态)

表结构

一对多多态关系类似于简单的一对多关系;然而,目标模型可以在单个关联上属于多个类型的模型。例如,想象您的应用程序用户可以对文章和视频进行“评论”。使用多态关系,您可以为这两种情况使用单个 comments 表。首先,让我们检查构建此关系所需的表结构:

php
posts
    id - integer
    title - string
    body - text

videos
    id - integer
    title - string
    url - string

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

模型结构

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

php
<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

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

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

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

检索关系

一旦定义了数据库表和模型,您可以通过模型访问关系。例如,要访问文章的所有评论,我们可以使用 comments 动态属性:

php
$post = App\Post::find(1);

foreach ($post->comments as $comment) {
    //
}

您还可以通过访问执行 morphTo 调用的方法名称从多态模型中检索所有者。在我们的例子中,这是 Comment 模型上的 commentable 方法。因此,我们将访问该方法作为动态属性:

php
$comment = App\Comment::find(1);

$commentable = $comment->commentable;

Comment 模型上的 commentable 关系将返回 PostVideo 实例,具体取决于哪个类型的模型拥有评论。

多对多(多态)

表结构

多对多多态关系比 morphOnemorphMany 关系稍微复杂一些。例如,博客 PostVideo 模型可以共享与 Tag 模型的多态关系。使用多对多多态关系允许您拥有一个在博客文章和视频中共享的唯一标签列表。首先,让我们检查表结构:

php
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

模型结构

接下来,我们准备在模型上定义关系。PostVideo 模型都将有一个 tags 方法,该方法调用基类 Eloquent 的 morphToMany 方法:

php
<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Post extends Model
{
    /**
     * 获取文章的所有标签。
     */
    public function tags()
    {
        return $this->morphToMany('App\Tag', 'taggable');
    }
}

定义关系的反向

接下来,在 Tag 模型上,您应该为其每个相关模型定义一个方法。因此,对于此示例,我们将定义一个 posts 方法和一个 videos 方法:

php
<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

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

    /**
     * 获取分配此标签的所有视频。
     */
    public function videos()
    {
        return $this->morphedByMany('App\Video', 'taggable');
    }
}

检索关系

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

php
$post = App\Post::find(1);

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

您还可以通过访问执行 morphedByMany 调用的方法名称从多态模型中检索所有者。在我们的例子中,这是 Tag 模型上的 postsvideos 方法。因此,您将访问这些方法作为动态属性:

php
$tag = App\Tag::find(1);

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

自定义多态类型

默认情况下,Laravel 将使用完全限定的类名来存储相关模型的类型。例如,给定上面的一个多对多示例,其中 Comment 可能属于 PostVideo,默认的 commentable_type 将分别是 App\PostApp\Video。但是,您可能希望将数据库与应用程序的内部结构解耦。在这种情况下,您可以定义一个“morph map”来指示 Eloquent 使用自定义名称而不是类名:

php
use Illuminate\Database\Eloquent\Relations\Relation;

Relation::morphMap([
    'posts' => 'App\Post',
    'videos' => 'App\Video',
]);

您可以在 AppServiceProviderboot 函数中注册 morphMap,或者如果您愿意,可以创建一个单独的服务提供者。

exclamation

在将“morph map”添加到现有应用程序时,数据库中仍包含完全限定类的每个多态 *_type 列值都需要转换为其“map”名称。

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

php
use Illuminate\Database\Eloquent\Relations\Relation;

$alias = $post->getMorphClass();

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

动态关系

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

php
use App\Order;
use App\Customer;

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

在定义动态关系时,请始终为 Eloquent 关系方法提供显式键名参数。

查询关系

由于所有类型的 Eloquent 关系都是通过方法定义的,您可以调用这些方法以获取关系的实例,而无需实际执行关系查询。此外,所有类型的 Eloquent 关系也作为查询构建器,允许您在最终对数据库执行 SQL 之前继续在关系查询上链式添加约束。

例如,想象一个博客系统,其中 User 模型有许多关联的 Post 模型:

php
<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

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

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

php
$user = App\User::find(1);

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

您可以在关系上使用任何查询构建器方法,因此请务必探索查询构建器文档以了解所有可用的方法。

在关系后链式添加 orWhere 子句

如上例所示,您可以在查询关系时自由添加额外的约束。但是,在关系上链式添加 orWhere 子句时要小心,因为 orWhere 子句将在与关系约束相同的级别上逻辑分组:

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

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

在大多数情况下,您可能希望使用约束组在括号之间逻辑分组条件检查:

php
use Illuminate\Database\Eloquent\Builder;

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

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

关系方法与动态属性

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

php
$user = App\User::find(1);

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

动态属性是“延迟加载”的,这意味着它们只会在您实际访问它们时加载其关系数据。因此,开发人员通常使用预加载来预加载他们知道在加载模型后将访问的关系。预加载显著减少了加载模型关系所需执行的 SQL 查询数量。

查询关系存在

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

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

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

php
// 检索所有至少有三个评论的文章...
$posts = App\Post::has('comments', '>=', 3)->get();

嵌套的 has 语句也可以使用“点”符号构造。例如,您可以检索所有至少有一个评论和投票的文章:

php
// 检索至少有一个带有投票的评论的文章...
$posts = App\Post::has('comments.votes')->get();

如果您需要更强大的功能,可以使用 whereHasorWhereHas 方法在 has 查询上添加“where”条件。这些方法允许您向关系约束添加自定义约束,例如检查评论的内容:

php
use Illuminate\Database\Eloquent\Builder;

// 检索至少有一个评论包含类似 foo% 的词的文章...
$posts = App\Post::whereHas('comments', function (Builder $query) {
    $query->where('content', 'like', 'foo%');
})->get();

// 检索至少有十个评论包含类似 foo% 的词的文章...
$posts = App\Post::whereHas('comments', function (Builder $query) {
    $query->where('content', 'like', 'foo%');
}, '>=', 10)->get();

查询关系不存在

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

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

如果您需要更强大的功能,可以使用 whereDoesntHaveorWhereDoesntHave 方法在 doesntHave 查询上添加“where”条件。这些方法允许您向关系约束添加自定义约束,例如检查评论的内容:

php
use Illuminate\Database\Eloquent\Builder;

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

您可以使用“点”符号对嵌套关系执行查询。例如,以下查询将检索没有评论的文章和评论来自未被禁止的作者的文章:

php
use Illuminate\Database\Eloquent\Builder;

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

查询多态关系

要查询 MorphTo 关系的存在,您可以使用 whereHasMorph 方法及其对应的方法:

php
use Illuminate\Database\Eloquent\Builder;

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

// 检索与标题不类似 foo% 的文章相关的评论...
$comments = App\Comment::whereDoesntHaveMorph(
    'commentable',
    'App\Post',
    function (Builder $query) {
        $query->where('title', 'like', 'foo%');
    }
)->get();

您可以使用 $type 参数根据相关模型添加不同的约束:

php
use Illuminate\Database\Eloquent\Builder;

$comments = App\Comment::whereHasMorph(
    'commentable',
    ['App\Post', 'App\Video'],
    function (Builder $query, $type) {
        $query->where('title', 'like', 'foo%');

        if ($type === 'App\Post') {
            $query->orWhere('content', 'like', 'foo%');
        }
    }
)->get();

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

php
use Illuminate\Database\Eloquent\Builder;

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

计数相关模型

如果您想在不实际加载结果的情况下计算关系的结果数量,可以使用 withCount 方法,该方法将在结果模型上放置一个 {relation}_count 列。例如:

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

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

您可以为多个关系添加“计数”,并为查询添加约束:

php
use Illuminate\Database\Eloquent\Builder;

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

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

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

php
use Illuminate\Database\Eloquent\Builder;

$posts = App\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;

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

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

echo $posts[0]->title;
echo $posts[0]->body;
echo $posts[0]->comments_count;

此外,使用 loadCount 方法,您可以在父模型已经被检索后加载关系计数:

php
$book = App\Book::first();

$book->loadCount('genres');

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

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

计数多态关系上的相关模型

如果您希望预加载 morphTo 关系,以及该关系可能返回的各种实体上的嵌套关系计数,您可以使用 with 方法结合 morphTo 关系的 morphWithCount 方法。

在此示例中,假设 PhotoPost 模型可能创建 ActivityFeed 模型。此外,假设 Photo 模型与 Tag 模型相关联,而 Post 模型与 Comment 模型相关联。

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

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

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

此外,您可以使用 loadMorphCount 方法在 ActivityFeed 模型已经被检索后预加载多态关系的所有嵌套关系计数:

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

预加载

在访问 Eloquent 关系作为属性时,关系数据是“延迟加载”的。这意味着关系数据在您首次访问属性之前不会实际加载。然而,Eloquent 可以在您查询父模型时“预加载”关系。预加载缓解了 N + 1 查询问题。为了说明 N + 1 查询问题,考虑一个与 Author 相关的 Book 模型:

php
<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Book extends Model
{
    /**
     * 获取撰写书籍的作者。
     */
    public function author()
    {
        return $this->belongsTo('App\Author');
    }
}

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

php
$books = App\Book::all();

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

此循环将执行 1 个查询以检索表上的所有书籍,然后为每本书执行另一个查询以检索作者。因此,如果我们有 25 本书,此循环将运行 26 个查询:1 个用于原始书籍,25 个额外的查询用于检索每本书的作者。

幸运的是,我们可以使用预加载将此操作减少到仅 2 个查询。在查询时,您可以使用 with 方法指定应预加载哪些关系:

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

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

对于此操作,将仅执行两个查询:

php
select * from books

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

预加载多个关系

有时您可能需要在单个操作中预加载多个不同的关系。为此,只需将额外的参数传递给 with 方法:

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

嵌套预加载

要预加载嵌套关系,您可以使用“点”语法。例如,让我们在一个 Eloquent 语句中预加载所有书籍的作者和所有作者的个人联系人:

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

嵌套预加载 morphTo 关系

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

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

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

php
$books = App\Book::with('author:id,name')->get();
exclamation

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

默认预加载

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

php
<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Book extends Model
{
    /**
     * 应始终加载的关系。
     *
     * @var array
     */
    protected $with = ['author'];

    /**
     * 获取撰写书籍的作者。
     */
    public function author()
    {
        return $this->belongsTo('App\Author');
    }
}

如果您希望在单个查询中从 $with 属性中删除一个项目,可以使用 without 方法:

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

约束预加载

有时您可能希望预加载关系,但也为预加载查询指定额外的查询条件。以下是一个示例:

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

在此示例中,Eloquent 将仅预加载标题列包含单词 first 的文章。您可以调用其他查询构建器方法以进一步自定义预加载操作:

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

在约束预加载时,不能使用 limittake 查询构建器方法。

懒惰预加载

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

php
$books = App\Book::all();

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

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

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

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

php
public function format(Book $book)
{
    $book->loadMissing('author');

    return [
        'name' => $book->name,
        'author' => $book->author->name,
    ];
}

嵌套懒惰预加载和 morphTo

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

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

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

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

插入和更新相关模型

save 方法

Eloquent 提供了方便的方法来将新模型添加到关系中。例如,也许您需要为 Post 模型插入一个新的 Comment。而不是手动设置 post_id 属性在 Comment 上,您可以直接从关系的 save 方法中插入 Comment

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

$post = App\Post::find(1);

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

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

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

php
$post = App\Post::find(1);

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

savesaveMany 方法不会将新模型添加到已经加载到父模型上的任何内存关系中。如果您计划在使用 savesaveMany 方法后访问关系,您可能希望使用 refresh 方法重新加载模型及其关系:

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

$post->refresh();

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

递归保存模型和关系

如果您希望 save 您的模型及其所有关联关系,可以使用 push 方法:

php
$post = App\Post::find(1);

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

$post->push();

create 方法

除了 savesaveMany 方法,您还可以使用 create 方法,该方法接受一个属性数组,创建一个模型,并将其插入到数据库中。同样,savecreate 之间的区别在于 save 接受一个完整的 Eloquent 模型实例,而 create 接受一个普通的 PHP array

php
$post = App\Post::find(1);

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

在使用 create 方法之前,请务必查看有关属性批量赋值的文档。

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

php
$post = App\Post::find(1);

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

您还可以使用 findOrNewfirstOrNewfirstOrCreateupdateOrCreate 方法在关系上创建和更新模型

属于关系

在更新 belongsTo 关系时,您可以使用 associate 方法。此方法将设置子模型上的外键:

php
$account = App\Account::find(10);

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

$user->save();

在删除 belongsTo 关系时,您可以使用 dissociate 方法。此方法将关系的外键设置为 null

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

$user->save();

默认模型

belongsTohasOnehasOneThroughmorphOne 关系允许您定义一个默认模型,如果给定关系为 null,将返回此模型。此模式通常称为空对象模式,可以帮助消除代码中的条件检查。在以下示例中,如果没有用户附加到文章,user 关系将返回一个空的 App\User 模型:

php
/**
 * 获取文章的作者。
 */
public function user()
{
    return $this->belongsTo('App\User')->withDefault();
}

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

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

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

多对多关系

附加/分离

Eloquent 还提供了一些额外的辅助方法,使处理相关模型更加方便。例如,假设一个用户可以有多个角色,一个角色可以有多个用户。要通过在连接模型的中间表中插入记录来将角色附加到用户,请使用 attach 方法:

php
$user = App\User::find(1);

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

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

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

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

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

// 从用户中分离所有角色...
$user->roles()->detach();

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

php
$user = App\User::find(1);

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

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

同步关联

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

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

您还可以与 ID 一起传递额外的中间表值:

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

如果您不想分离现有的 ID,可以使用 syncWithoutDetaching 方法:

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

切换关联

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

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

在枢纽表上保存额外数据

在处理多对多关系时,save 方法接受一个额外的中间表属性数组作为其第二个参数:

php
App\User::find(1)->roles()->save($role, ['expires' => $expires]);

更新枢纽表上的记录

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

php
$user = App\User::find(1);

$user->roles()->updateExistingPivot($roleId, $attributes);

触碰父级时间戳

当一个模型 belongsTobelongsToMany 另一个模型时,例如一个 Comment 属于一个 Post,有时在子模型更新时更新父级的时间戳是有帮助的。例如,当更新 Comment 模型时,您可能希望自动“触碰”拥有的 Postupdated_at 时间戳。Eloquent 使这变得简单。只需在子模型中添加一个包含关系名称的 touches 属性:

php
<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

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

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

现在,当您更新 Comment 时,拥有的 Post 也将更新其 updated_at 列,这使得更方便地知道何时使 Post 模型的缓存失效:

php
$comment = App\Comment::find(1);

$comment->text = 'Edit to this comment!';

$comment->save();