模型关联

未匹配的标注

Eloquent: 关联

简介

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

定义关联

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

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

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

注意:关系名称不能与属性名称冲突,因为这会导致您的模型不知道要解析哪一个。

一对一

一对一是最基本的关联关系。例如,一个 User 模型可能关联一个 Phone 模型。为了定义这个关联,我们要写一个 phone 方法,在 User 模型中。 在 phone 方法内部调用 hasOne 方法并返回其结果:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

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

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

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

Eloquent 会基于模型名决定外键名称。在这种情况下,会自动假设 Phone 模型有一个 user_id 外键。如果你想覆盖这个约定,可以传递第二个参数给 hasOne 方法:

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

另外,Eloquent 假设外键的值是与父级 id (或自定义 $primaryKey)列的值相匹配的。换句话说,Eloquent 将会通过 Phone 记录的 user_id 列中查找与用户表的 id 列相匹配的值。如果您希望该关联使用 id 以外的自定义键名,就可以给 hasOne 方法传递第三个参数:

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

定义反向关联

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

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

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

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

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

如果父级模型没有使用 id 作为主键,或者是希望使用不同的字段来连接子级模型,则可以通过给 belongsTo 方法传递第三个参数的形式指定父级数据表的自定义键:

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

一对多

一对多关联用于定义单个模型拥有任意数量的其它关联模型。例如,一篇博客文章可能会有无限条评论。正如其它所有的 Eloquent 关联一样,一对多关联的定义也是在 Eloquent 模型中写一个方法:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

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

记住一点,Eloquent 将会自动确定 Comment 模型的外键属性。按照约定,Eloquent 将会使用所属模型名称的「Snake Case」 形式,再加上 _id 后缀作为外键字段。因此,在上面这个例子中,Eloquent 将假定 Comment 模型对应到 Post 模型上的外键就是 post_id

一旦关系被定义好以后,就可以通过访问 Post 模型的 comments 属性来获取评论的集合。记住,由于 Eloquent 提供了「动态属性」,因此我们可以像访问模型的属性一样访问关联方法:

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

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

当然,由于所有的关联还可以作为查询语句构造器使用,因此你可以使用链式调用的方式,在 comments 方法上添加额外的约束条件:

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

正如 hasOne 方法一样,你也可以通过向 hasMany 方法传递附加参数来覆盖默认的外键和本地键:

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

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

一对多 (反向)

现在我们已经可以访问文章中的所有评论了,让我们再定义一个关系,以允许评论 (comment) 获取它所属的文章 (post) 。 这个关联则是 hasMany 关联的反向关联, 需要在子模型中使用 belongsTo 方法来定义它:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

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

在这个关系定义好后,我们就可以通过访问 Comment 模型中的「动态属性」 post 来获取关联的 Post 模型了:

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

echo $comment->post->title;

在上面的例子中,Eloquent 将会尝试匹配 Comment 模型中的 post_idPost 模型中的 id 。 Eloquent 会通过检查关联方法名,并在该关联方法名后方加上 _ 再加上主键后缀名来确定默认外键名 (foreign_key) 。 当然,如果 Comment 模型的外键名不是 post_id, 你也可以通过向 belongsTo 方法传递第二个参数以作为自定义键名:

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

如果你的父级数据表不使用 id 作为它的主键, 或者你希望用不同的字段来连接子级模型, 你可以通过向 belongsTo 方法传递三个参数的形式来指定父级数据表的自定义键:

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

多对多

多对多关联比 hasOnehasMany 关联稍微复杂些。举个例子,一个用户可以拥有很多种角色,同时这些角色也被其他用户共享。例如,许多用户可能都有 「管理员」 这个角色。

表结构

要定义这种关联,需要三个数据库表: usersrolesrole_userrole_user 表的命名是由关联的两个模型按照字母顺序来的,并且包含了 user_idrole_id 字段:

users
    id - integer
    name - string

roles
    id - integer
    name - string

role_user
    user_id - integer
    role_id - integer

模型结构

多对多关联通过调用 belongsToMany 这个内部方法返回的结果来定义,例如,我们在 User 模型中定义 roles 方法:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

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

一旦关联关系被定义后,你可以通过 roles 「动态属性」获取用户角色:

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

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

当然,像其它所有关联模型一样,你可以使用 roles 方法,利用链式调用对查询语句添加约束条件:

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

正如前面所提到的,为了确定关联连接表的表名,Eloquent 会按照字母顺序连接两个关联模型的名字。当然,你也可以不使用这种约定,传递第二个参数到 belongsToMany 方法即可:

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

除了自定义连接表的表名,你还可以通过传递额外的参数到 belongsToMany 方法来定义该表中字段的键名。第三个参数是定义此关联的模型在连接表里的外键名,第四个参数是另一个模型在连接表里的外键名:

return $this->belongsToMany('App\Models\Role', '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('App\Models\User');
    }
}

如你所见,除了引入模型为 App\Models\User 外,其它与在 User 模型中定义的完全一样。由于我们重用了 belongsToMany 方法,自定义连接表表名和自定义连接表里的键的字段名称在这里同样适用。

获取中间表字段

就如你刚才所了解的一样,多对多的关联关系需要一个中间表来提供支持, Eloquent 提供了一些有用的方法来和这张表进行交互。例如,假设我们的 User 对象关联了多个 Role 对象。在获得这些关联对象后,可以使用模型的 pivot 属性访问中间表的属性:

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

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

需要注意的是,我们获取的每个 Role 模型对象,都会被自动赋予 pivot 属性,它代表中间表的一个模型对象,并且可以像其他的 Eloquent 模型一样使用。

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

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

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

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

自定义 pivot 属性名称

如前所述,来自中间表的属性可以使用 pivot 属性访问。但是,你可以自由定制此属性的名称,以便更好的反应其在应用中的用途。

例如,如果你的应用中包含可能订阅的用户,则用户与博客之间可能存在多对多的关系。如果是这种情况,你可能希望将中间表访问器命名为 subscription 取代 pivot 。这可以在定义关系时使用 as 方法完成:

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

一旦定义完成,你可以使用自定义名称访问中间表数据:

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

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

通过中间表过滤关系

在定义关系时,你还可以使用 wherePivotwherePivotIn 方法来过滤 belongsToMany 返回的结果:

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

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

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

定义中间表模型

如果你想定义一个自定义模型来表示关联关系中的中间表,可以在定义关联时调用 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('App\Models\User')->using('App\Models\RoleUser');
    }
}

当定义 RoleUser 模型时,我们要扩展 Pivot 类:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Relations\Pivot;

class RoleUser extends Pivot
{
    //
}

你可以组合使用 usingwithPivot 从中间表来检索列。例如,通过将列名传递给 withPivot 方法,就可以从 UserRole 中间表中检索出 created_byupdated_by 两列数据:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

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

注意: Pivot 模型可能不使用 SoftDeletes 特性。 如果您需要软删除数据关联记录,请考虑将您的数据关联模型转换为实际的 Eloquent 模型。

带有递增 ID 的自定义中继模型

如果你用一个自定义的中继模型定义了多对多的关系,而且这个中继模型拥有一个自增的主键,你应当确保这个自定义中继模型类中定义了一个 incrementing 属性其值为 true

/**
 * 标识 ID 是否自增
 *
 * @var bool
 */
public $incrementing = true;

远程一对一

远程一对一关联通过一个中间关联模型实现。

例如,一个车辆维修应用中,每个修理工「Mechanic」 负责一辆汽车「Car」, 并且每辆汽车「Car」属于一个车主「Owner」。虽然修理工「Mechanic」和车主「Owner」没有直接联系,但是修理工「Mechanic」可以通过汽车「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('App\Models\Owner', 'App\Models\Car');
    }
}

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

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

class Mechanic extends Model
{
    /**
     * 获取车主信息
     */
    public function carOwner()
    {
        return $this->hasOneThrough(
            'App\Models\Owner',
            'App\Models\Car',
            'mechanic_id', // 汽车表外键...
            'car_id', // 车主表外键...
            'id', // 修理工表本地键...
            'id' // 汽车表本地键...
        );
    }
}

远程一对多关联

远程「一对多」关联提供了方便、简短的方式通过中间的关联来获得远层的关联。例如,一个 Country 模型可以通过中间的 User 模型获得多个 Post 模型。在这个例子中,你可以轻易地收集给定国家的所有博客文章。让我们来看看定义这种关联所需的数据表:

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 会先检查中间表 userscountry_id 字段,找到所有匹配的用户 ID 后,使用这些 ID,在 posts 表中完成查找。

现在,我们已经知道了定义这种关联所需的数据表结构,接下来,让我们在 Country 模型中定义它:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

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

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

当执行关联查询时,通常会使用 Eloquent 约定的外键名。如果你想要自定义关联的键,可以通过给 hasManyThrough 方法传递第三个和第四个参数实现,第三个参数表示中间模型的外键名,第四个参数表示最终模型的外键名。第五个参数表示本地键名,而第六个参数表示中间模型的本地键名:

class Country extends Model
{
    public function posts()
    {
        return $this->hasManyThrough(
            'App\Models\Post',
            'App\Models\User',
            'country_id', // 国家表外键...
            'user_id', // 用户表外键...
            'id', // 国家表本地键...
            'id' // 用户表本地键...
        );
    }
}

多态关联

多态关联允许目标模型借助单个关联从属于多个模型。

一对一 (多态)

表结构

一对一多态关联与简单的一对一关联类似;不过,目标模型能够在一个关联上从属于多个模型。例如,博客 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('App\Models\Image', 'imageable');
    }
}

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

获取关联

一旦定义了表和模型,就可以通过模型访问此关联。比如,要获取文章图片,可以使用 image 动态属性:

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

$image = $post->image;

还可以通过访问执行 morphTo 调用的方法名来从多态模型中获知父模型。在这个例子中,就是 Image 模型的 imageable 方法。所以,我们可以像动态属性那样访问这个方法:

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

$imageable = $image->imageable;

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

/**
 * 获取 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
{
    /**
     * 获取拥有此评论的模型
     */
    public function commentable()
    {
        return $this->morphTo();
    }
}

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

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

获取关联

一旦定义了数据库表和模型,就可以通过模型访问关联。例如,可以使用 comments 动态属性访问文章的全部评论:

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

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

您还可以通过访问执行对 morphTo 的调用的方法名来从多态模型获取其所属模型。在我们的例子中,这就是Comment模型上的commentable方法。 因此,我们将以动态属性的形式访问该方法:

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

$commentable = $comment->commentable;

Comment 模型的 commentable 关联将返回 PostVideo 实例,其结果取决于评论所属的模型。

多对多(多态)

表结构

多对多多态关联比 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

模型结构

接下来,在模型上定义关联。PostVideo 模型都有调用 Eloquent 基类上 morphToMany 方法的 tags 方法:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

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

定义反向关联关系

下面,需要在 Tag 模型上为每个关联模型定义一个方法。在这个示例中,我们将会定义 posts 方法和 videos 方法:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

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

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

获取关联

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

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

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

还可以访问执行 morphedByMany 方法调用的方法名来从多态模型获取其所属模型。在这个示例中,就是 Tag 模型的 postsvideos 方法。可以像动态属性一样访问这些方法:

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

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

自定义多态类型

默认情况下, Laravel 使用完全限定类名存储关联模型类型。在上面的一对多示例中, 因为 Comment 可能从属于一个 Post 或一个 Video,默认的 commentable_type 就将分别是 App\PostApp\Video。不过,你可能希望数据库与应用的内部结构解耦。在这种情况下,可以定义一个 「morph 映射」 来通知 Eloquent 使用自定义名称代替对应的类名:

use Illuminate\Database\Eloquent\Relations\Relation;

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

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

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

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

use Illuminate\Database\Eloquent\Relations\Relation;

$alias = $post->getMorphClass();

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

动态关联

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

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('App\Models\Post');
    }
}

你可以查询 posts 关联,并为其添加额外的约束:

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

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

你可以在关联上使用任意 查询构造器 方法,请查阅查询构造器文档,学习那些对你有用的方法。

在关联之后链式添加 orWhere 条件

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

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

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

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

关联方法 Vs 动态属性

如果想访问 Eloquent 关联的所有记录,而不附带查询条件,可以像属性一样访问关联,以 UserPost 模型为例,可以这样访问用户的所有文章

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

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

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

查询已存在的关联

在查询时,可能需要将关联记录是否存在作为条件。例如,查出至少有一条评论的文章。可以通过向 hasorHas 方法传递关联名称实现:

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

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

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

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

// 查出至少有一条带投票评论的文章...
$posts = App\Models\Post::has('comments.votes')->get();

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

use Illuminate\Database\Eloquent\Builder;

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

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

查询不存在的关联

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

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

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

use Illuminate\Database\Eloquent\Builder;

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

还可以使用 「点」 语法执行嵌套关联查询。例如,下面的查询用于获取带有没被禁用的作者发表评论的文章:

use Illuminate\Database\Eloquent\Builder;

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

多态关联查询

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

use Illuminate\Database\Eloquent\Builder;

// 查询与帖子或视频相关并且标题包含 foo 的评论...
$comments = App\Models\Comment::whereHasMorph(
    'commentable',
    ['App\Models\Post', 'App\Models\Video'],
    function (Builder $query) {
        $query->where('title', 'like', 'foo%');
    }
)->get();

// 查询与帖子相关的评论,标题不包含 foo%...
$comments = App\Models\Comment::whereDoesntHaveMorph(
    'commentable',
    'App\Models\Post',
    function (Builder $query) {
        $query->where('title', 'like', 'foo%');
    }
)->get();

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

use Illuminate\Database\Eloquent\Builder;

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

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

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

use Illuminate\Database\Eloquent\Builder;

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

关联模型计数

如果想要只计算关联结果的统计数量而不需要真实加载它们,可以使用 withCount 方法,它将放在结果模型的 {relation}_count 列。示例如下:

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

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

可以像给查询添加限制一样为多个关系添加「计数」:

use Illuminate\Database\Eloquent\Builder;

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

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

还可以给关联计数结果起别名,这允许你在同一关联上添加多个计数:

use Illuminate\Database\Eloquent\Builder;

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

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

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

此外,使用 loadCount 方法,您可以在父模型被加载后使用关联计数:

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

$book->loadCount('genres');

如果您需要在预加载查询上设置额外的查询约束,您可以传递一个希望加载的关联数组。数组值应该是 Closure 实例,它接收查询生成器实例:

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

多态关联计数

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

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

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

use Illuminate\Database\Eloquent\Relations\MorphTo;

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

另外,如果已经检索了 ActivityFeed 模型,则可以使用 loadMorphCount 方法将所有嵌套的关系计数加载到多态关系的各个实体上:

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

预加载

当以属性方式访问 Eloquent 关联时,关联数据「懒加载」。这意味着直到第一次访问属性时关联数据才会被真实加载。不过 Eloquent 能在查询父模型时「预先载入」子关联。预加载可以缓解 N + 1 查询问题。为了说明 N + 1 查询问题,考虑 Book 模型关联到 Author 的情形:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

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

现在,我们来获取所有的书籍及其作者:

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

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

此循环将执行一个查询,用于获取全部书籍,然后为每本书执行获取作者的查询。如果我们有 25 本书,此循环将运行 26 个查询:1 个用于查询书籍,25 个附加查询用于查询每本书的作者。

谢天谢地,我们能够使用预加载将操作压缩到只有 2 个查询。在查询时,可以使用 with 方法指定想要预加载的关联:

$books = App\Models\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 = App\Models\Book::with(['author', 'publisher'])->get();

嵌套预加载

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

$books = App\Models\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 = App\Models\Book::with('author:id,name')->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('App\Models\Author');
    }
}

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

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

为预加载添加约束

有时,你可能希望预加载一个关联,同时为预加载查询添加额外查询条件,就像下面的例子:

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

在这个例子中, Eloquent 将仅预加载那些 title 列包含 first 关键词的文章。也可以调用其它的 查询构造器 方法进一步自定义预加载操作:

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

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

延迟预加载

有可能你还希望在模型加载完成后在进行渴求式加载。举例来说,如果你想要根据某个条件动态决定是否加载关联数据,那么 load 方法对你来说会非常有用:

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

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

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

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

如果希望加载还没有加载的关联关系时,你可以使用 loadMissing 方法:

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

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

嵌套延迟预加载 & morphTo

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

这个方法接受 morphTo 关系的名称作为它的第一个参数,第二个参数接收模型数组、关系数组。为了帮助说明这个方法,可以看一下以下模型例子:

<?php

use Illuminate\Database\Eloquent\Model;

class ActivityFeed extends Model
{
    /**
     * 获取 activity feed 记录的父级
     */
    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'],
    ]);

插入 & 更新关联模型

保存方法

Eloquent 为新模型添加关联提供了便捷的方法。例如,也许你需要添加一个新的 Comment 到一个 Post 模型中。你不用在 Comment 中手动设置 post_id 属性,就可以直接使用关联模型的 save 方法将 Comment 直接插入:

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

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

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

需要注意的是,我们并没有使用动态属性的方式访问 comments 关联。相反,我们调用 comments 方法来获得关联实例。save 方法将自动添加适当的 post_id 值到 Comment 模型中。

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

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

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

savesaveMany方法不会将新模型加载到父模型上。 如果你想在使用savesaveMany方法之后访问该关联,需要使用refresh方法来重新加载模型及其关联:

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

$post->refresh();

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

递归保存模型和关联数据

如果你想 save 你的模型及其所有关联数据,你可以使用 push 方法:

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

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

$post->push();

新增方法

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

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

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

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

你还可以使用 createMany 方法去创建多个关联模型:

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

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

你还可以使用 findOrNewfirstOrNewfirstOrCreateupdateOrCreate 方法来 创建和更新关系模型

更新 belongsTo 关联

当更新 belongsTo 关联时,可以使用 associate 方法。此方法将会在子模型中设置外键:

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

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

$user->save();

当移除 belongsTo 关联时,可以使用 dissociate 方法。此方法会将关联外键设置为 null

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

$user->save();

Default Models

belongsTohasOnehasOneThroughmorphOne 关系允许你指定默认模型,当给定关系为 null 时,将会返回默认模型。 这种模式被称作 空对象模式 ,可以减少你代码中不必要的检查。在下面的例子中,如果发布的帖子没有找到作者, user 方法会返回一个空的 App\Models\User 模型:

/**
 * 获取帖子的作者
 */
public function user()
{
    return $this->belongsTo('App\Models\User')->withDefault();
}

如果需要在默认模型里添加属性, 你可以传递数组或者回调方法到 withDefault 中:

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

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

多对多关联

附加 / 分离

Eloquent 也提供了一些额外的辅助方法,使相关模型的使用更加方便。例如,我们假设一个用户可以拥有多个角色,并且每个角色都可以被多个用户共享。给某个用户附加一个角色是通过向中间表插入一条记录实现的,可以使用 attach 方法完成该操作:

$user = App\Models\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 = App\Models\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,可以使用 syncWithoutDetaching 方法:

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

切换关联

多对多关联也提供了 toggle 方法用于「切换」给定 ID 数组的附加状态。 如果给定的 ID 已被附加在中间表中,那么它将会被移除,同样,如果给定的 ID 已被移除,它将会被附加:

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

在中间表上保存额外的数据

当处理多对多关联时,save 方法接收一个额外的数据数组作为第二个参数:

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

更新中间表记录

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

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

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

更新父级时间戳

当一个模型属 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('App\Models\Post');
    }
}

现在,当你更新 Comment 时,对应父级 Post 模型的 updated_at 字段同时也会被更新,使其更方便得知何时让一个 Post 模型的缓存失效:

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

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

$comment->save();

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

本译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接
我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。
上一篇 下一篇
Summer
《L01 基础入门》
我们将带你从零开发一个项目并部署到线上,本课程教授 Web 开发中专业、实用的技能,如 Git 工作流、Laravel Mix 前端工作流等。
《L04 微信小程序从零到发布》
从小程序个人账户申请开始,带你一步步进行开发一个微信小程序,直到提交微信控制台上线发布。
贡献者:9
讨论数量: 0
发起讨论 只看当前版本


暂无话题~