关联关系


简介

数据表经常要与其它表做关联,比如一篇博客文章可能有很多评论,或者一个订单会被关联到下单用户,Eloquent 让组织和处理这些关联关系变得简单,并且支持多种不同类型的关联关系:

  • 一对一
  • 一对多
  • 多对多
  • 远层一对多
  • 远层多对多
  • 一对一(多态)
  • 一对多(多态)
  • 多对多(多态)

定义关联关系

Eloquent 关联关系以 Eloquent 模型类方法的方式定义。和 Eloquent 模型本身一样,关联关系也是强大的查询构建器,定义关联关系为方法可以提供功能强大的方法链和查询能力。例如,我们可以添加更多约束条件到 posts 关联关系:

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

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

一对一

一对一关联是一个非常简单的关联关系,例如,一个 User 模型有一个与之关联的 Phone 模型。要定义这种关联关系,我们需要将 phone 方法置于User 模型中,phone 方法会调用 Illuminate\Database\Eloquent\Concerns\HasRelationships trait 中的 hasOne 方法并返回其结果:

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class User extends Model{
    /**
     * 获取关联到用户的手机
     */
    public function phone()
    {
        return $this->hasOne('App\Phone');
    }
}

传递给 hasOne 方法的第一个参数是关联模型的名称,关联关系被定义后,我们可以使用 Eloquent 的动态属性获取关联记录。动态属性允许我们访问关联方法,就像它们是定义在模型上的属性一样:

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

Eloquent 默认关联关系的外键基于模型名称,在本例中,Phone 模型默认有一个 user_id 外键,如果你希望覆盖这种约定,可以传递第二个参数到 hasOne 方法:

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

此外,Eloquent 假设外键应该在父级上有一个与之匹配的 id(或者自定义 $primaryKey),换句话说,Eloquent 将会通过 user 表的 id 值去 phone 表中查询 user_id 与之匹配的 Phone 记录。如果你想要关联关系使用其他值而不是 id,可以传递第三个参数到hasOne 来指定自定义的主键:

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

我们通过传递完整参数改写上述示例代码就是:

return $this->hasOne('App\Phone', 'user_id', 'id');    

定义相对的关联

我们可以从 User 中访问 Phone 模型,相应地,也可以在 Phone 模型中定义关联关系从而让我们可以拥有该手机的 User。我们可以使用 belongsTo 方法定义与 hasOne 关联关系相对的关联:

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

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

在上面的例子中,Eloquent 默认将会尝试通过 Phone 模型的 user_idUser 模型查找与之匹配的记录。Eloquent 通过在关联关系方法名后加 _id 后缀来生成默认的外键名。不过,如果 Phone 模型上的外键不是 user_id,也可以将自定义的键名作为第二个参数传递到 belongsTo 方法:

/**
 * 获取手机对应的用户
 */
public function user(){
    return $this->belongsTo('App\User', 'foreign_key');
}

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

/**
 * 获取手机对应的用户
 */
public function user(){
    return $this->belongsTo('App\User', 'foreign_key', 'other_key');
}

同样,我们通过传递完整的参数来改写上述示例代码:

return $this->belongsTo('App\User', 'user_id', 'id');

一对多

“一对多”关联是用于定义单个模型拥有多个其它模型的关联关系。例如,一篇博客文章拥有多条评论,和其他关联关系一样,一对多关联通过在 Eloquent 模型中定义方法来定义:

<?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 提供了“动态属性”,我们可以像访问模型的属性一样访问关联方法:

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

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

当然,由于所有关联同时也是查询构建器,我们可以添加更多的条件约束到通过调用 comments 方法获取到的评论上:

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

hasOne 方法一样,你还可以通过传递额外参数到 hasMany 方法来重新设置外键和本地主键:

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

// 在本例中,传递完整参数代码如下
return $this->hasMany('App\Comment', 'post_id', 'id');

一对多(逆向)

现在我们可以访问文章的所有评论了,接下来让我们定义一个关联关系允许通过评论访问所属文章。要定义与 hasMany 相对的关联关系,需要在子模型中定义一个关联方法去调用 belongsTo 方法:

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

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

关联关系定义好之后,我们可以通过访问动态属性 post 来获取某条 Comment 对应的 Post

$comment = App\Comment::find(1);
echo $comment->post->title;

在上面这个例子中,Eloquent 尝试匹配 Comment 模型的 post_idPost 模型的 id,Eloquent 通过关联方法名加上 _id 后缀生成默认外键,当然,你也可以通过传递自定义外键名作为第二个参数传递到 belongsTo 方法,如果你的外键不是 post_id,或者你想自定义的话:

/**
 * 获取评论对应的博客文章
 */
public function post()
{
    return $this->belongsTo('App\Post', 'foreign_key');
}

如果你的父模型不使用 id 作为主键,或者你希望通过其他数据列来连接子模型,可以将自定义键名作为第三个参数传递给 belongsTo 方法:

/**
 * 获取评论对应的博客文章
 */
public function post()
{
    return $this->belongsTo('App\Post', 'foreign_key', 'other_key');
}

类似的,通过传递完整参数改写上述调用代码如下:

return $this->belongsTo('App\Post', 'post_id', 'id');

多对多

多对多关联比 hasOnehasMany 关联关系要稍微复杂一些。这种关联关系的一个例子就是在权限管理中,一个用户可能有多个角色,同时一个角色可能被多个用户共用。例如,很多用户可能都有一个“Admin”角色。要定义这样的关联关系,需要三张数据表:usersrolesrole_userrole_user 表按照关联模型名的字母顺序命名,并且包含 user_idrole_id 两个列。

多对多关联通过编写调用 belongsToMany 方法返回结果的方式来定义,例如,我们在 User 模型上定义 roles 方法:

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

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

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

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

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

当然,和所有其它关联关系类型一样,你可以调用 roles 方法来添加条件约束到关联查询上:

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

正如前面所提到的,为了确定关联关系连接表的表名,Eloquent 以字母顺序连接两个关联模型的名字。不过,你可以重写这种约定 —— 通过传递第二个参数到 belongsToMany 方法:

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

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

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

定义相对的关联关系

要定义与多对多关联相对的关联关系,只需在关联模型中调用一下 belongsToMany 方法即可。我们在 Role 模型中定义 users 方法:

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

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

正如你所看到的,定义的关联关系和与其对应的 User 中定义的一模一样,只是前者引用 App\Role,后者引用 App\User,由于我们再次使用了 belongsToMany 方法,所有的常用表和键自定义选项在定义与多对多相对的关联关系时都是可用的。

获取中间表字段

正如你已经了解到的,处理多对多关联要求一个中间表。Eloquent 提供了一些有用的方法来与这个中间表进行交互,例如,我们假设 User 对象有很多与之关联的 Role 对象,访问这些关联关系之后,我们可以使用这些模型上的 pivot 属性访问中间表:

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

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

注意我们获取到的每一个 Role 模型都被自动赋上了 pivot 属性。该属性包含一个代表中间表的模型,并且可以像其它 Eloquent 模型一样使用。

默认情况下,只有模型主键才能用在 pivot 对象上,如果你的 pivot 表包含额外的属性,必须在定义关联关系时进行指定:

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

如果你想要你的 pivot 表自动包含created_atupdated_at 时间戳,在关联关系定义时使用 withTimestamps 方法:

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

自定义 pivot 属性名

上面已经提到,我们可以通过在模型上使用 pivot 属性来访问中间表字段,此外,我们还可以在应用中自定义这个属性名称来提升可读性。

例如,如果你的应用包含已经订阅播客的用户,那么就会有一个用户与播客之间的多对多关联,在这个例子中,你可能希望将中间表访问器改为 subscription 来取代 pivot,这可以通过在定义关联关系时使用 as 方法来实现:

return $this->belongsToMany('App\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\Role')->wherePivot('approved', 1);

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

自定义中间表模型

如果你想要定义自定义的模型来表示关联关系中间表,可以在定义关联关系的时候调用 using 方法,所有用于表示关联关系中间表的自定义模型都必须继承自 Illuminate\Database\Eloquent\Relations\Pivot 类,用于自定义多态的多对多中间模型则继承自 Illuminate\Database\Eloquent\Relations\MorphPivot 类。例如,我们可以定义一个使用 RoleUser 中间模型的 Role

<?php

namespace App;

use Illuminate\Database\Relations\Pivot;

class Role extends Pivot
{
    /**
     * The users that belong to the role.
     */
    public function users()
    {
        return $this->belongsToMany('App\User')->using('App\RoleUser');
    }
}

RoleUser 继承自 Pivot 类:

<?php

namespace App;

use Illuminate\Database\Eloquent\Relations\Pivot;

class RoleUser extends Pivot
{
    //
}

你可以将 usingwithPivot 联合起来以便从中间表获取字段。例如,你可以通过传递列名到 withPivot 方法以便从 RoleUser 中间表获取 created_byupdated_by 字段:

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Role extends Model
{
    /**
     * The users that belong to the role.
     */
    public function users()
    {
        return $this->belongsToMany('App\User')
                        ->using('App\RoleUser')
                        ->withPivot([
                            'created_by',
                            'updated_by'
                        ]);
    }
}

注意:Pivot 模型不能使用 SoftDeletes trait,如果你需要软删除中间表记录,需要将中间模型转化为真正的 Eloquent 模型。

自定义中间模型和自增ID

如果你已经定义过一个使用自定义中间模型的多对多关联关系,并且这个中间模型有一个自增主键,需要确保自定义的中间模型类定义了一个被设置为 trueincrementing 属性:

/**
 * Indicates if the IDs are auto-incrementing.
 *
 * @var bool
 */
public $incrementing = true;

远层一对一

「远层一对一」关联通过单一中间关系链接模型,例如,如果每个供应商都有一个用户,同时每个用户都与一个用户历史记录相关联,这样,供应商模型就可以通过用户来访问用户的历史。下面我们来看看定义这个关联关系所需的数据表结构:

users
    id - integer
    supplier_id - integer

suppliers
    id - integer

history
    id - integer
    user_id - integer

尽管 history 数据表不包含 supplier_id 列,hasOneThrough 关联仍然可以为供应商提供对用户历史的访问。现在,我们已经知道了关联关系对应的数据表结构,接下来我们在 Supplier 模型上定义这个关联:

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Supplier extends Model
{
    /**
     * Get the user's history.
     */
    public function userHistory()
    {
        return $this->hasOneThrough('App\History', 'App\User');
    }
}

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

和前面几种关联关系一样,在执行这个关联查询时也会应用常规的 Eloquent 外键默认约定,如果你想要自定义关联关系使用的外键,可以将它们作为第三个和第四个参数传递到 hasOneThrough 方法。其中,第三个参数是中间模型的外键名称,第四个参数是最终要访问的模型的外键名称,hasOneThrough 方法还有第五个参数,默认是当前模型的主键名称,以及第六个参数,表示中间模型的主键名称:

class Supplier extends Model
{
    /**
     * Get the user's history.
     */
    public function userHistory()
    {
        return $this->hasOneThrough(
            'App\History',
            'App\User',
            'supplier_id', // Foreign key on users table...
            'user_id', // Foreign key on history table...
            'id', // Local key on suppliers table...
            'id' // Local key on users table...
        );
    }
}

远层一对多

「远层一对多」关联为通过中间关联访问远层的关联关系提供了一个便捷之道。例如,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 在中间表 $users 上检查 country_id,查找到相匹配的用户ID后,通过用户ID来查询 posts 表。

既然我们已经查看了该关联关系的数据表结构,接下来让我们在 Country 模型上进行定义:

<?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 方法。第三个参数是中间模型的外键名,第四个参数是最终模型的外键名,第五个参数是本地主键。

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 模型的多态关联。使用一对一多态关联,你可以拥有一个可用于博客文章和用户账户的唯一图片列表。首先,我们来定义表结构:

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 关联时,Eloquent 使用 imageable_type 字段来判定返回哪种类型的父模型(Post 还是 User)。

模型结构

接下来,我们来看看用于构建这个关联的模型定义:

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Image extends Model
{
    /**
     * Get all of the owning imageable models.
     */
    public function imageable()
    {
        return $this->morphTo();
    }
}

class Post extends Model
{
    /**
     * Get the post's image.
     */
    public function image()
    {
        return $this->morphOne('App\Image', 'imageable');
    }
}

class User extends Model
{
    /**
     * Get the user's image.
     */
    public function image()
    {
        return $this->morphOne('App\Image', 'imageable');
    }
}

获取关联关系

定义好数据表和模型类之后,就可以通过模型来访问关联关系了。例如,要获取某篇文章的图片,可以使用 image 动态属性:

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

$image = $post->image;

还可以从多态模型中通过访问调用 morphTo 的方法名来获取其归属的父模型。在这个例子中,就是 Image 模型的 imageable 方法,因此,我们可以通过动态属性的方式来访问该方法:

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

$imageable = $image->imageable;

Image 模型上的 imageable 关联将会返回 PostUser 实例,这取决于哪中模型拥有该图片。

一对多(多态)

表结构

一对多的多态关联和简单的一对多关联类似,不同之处在于其目标模型可以通过单个关联归属于多种模型。例如,假设应用用户既可以对文章进行评论也可以对视频进行评论,使用多态关联,你可以在这两种场景下使用单个 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

两个重要的需要注意的字段是 comments 表上的 commentable_idcommentable_typecommentable_id 字段对应 PostVideo 的 ID 值,而 commentable_type 字段对应所属模型的类名。当访问 commentable 关联时,ORM 根据 commentable_type 字段来判断所属模型的类型并返回相应模型实例。

模型结构

接下来,让我们看看构建这种关联关系需要在模型中定义什么:

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Comment extends Model
{
    /**
     * Get all of the owning commentable models.
     */
    public function commentable()
    {
        return $this->morphTo();
    }
}

class Post extends Model
{
    /**
     * Get all of the post's comments.
     */
    public function comments()
    {
        return $this->morphMany('App\Comment', 'commentable');
    }
}

class Video extends Model
{
    /**
     * Get all of the video's comments.
     */
    public function comments()
    {
        return $this->morphMany('App\Comment', 'commentable');
    }
}

获取多态关联

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

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

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

你还可以通过访问调用 morphTo 的方法名从多态模型中获取多态关联的所属对象。在本例中,就是 Comment 模型中的 commentable 方法。因此,我们可以用动态属性的方式访问该方法:

$comment = App\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 模型都有一个 tags 方法调用 Eloquent 基类的 morphToMany 方法:

<?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

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

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

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

还可以通过访问调用 morphedByMany 的方法名从多态模型中获取多态关联的所属对象。在本例中,就是 Tag 模型中的 posts 或者 videos 方法:

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

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

自定义多态类型

默认情况下,Laravel 使用完全限定类名(包含命名空间的完整类名)来存储关联模型的类型。举个例子,上面示例中的 Comment 可能属于某个 PostVideo,默认的 commentable_type 可能是 App\PostApp\Video。不过,有时候你可能需要解除数据库和应用内部结构之间的耦合,这样的情况下,可以定义一个 morphMap 关联来告知 Eloquent 为每个模型使用自定义名称替代完整类名:

use Illuminate\Database\Eloquent\Relations\Relation;

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

你可以在 AppServiceProviderboot 方法中注册这个 morphMap,如果需要的话,也可以创建一个独立的服务提供者来实现这一功能。

关联查询

由于 Eloquent 所有关联关系都是通过方法定义,你可以调用这些方法来获取关联关系的实例而不需要再去手动执行关联查询。此外,所有 Eloquent 关联关系类型同时也是查询构建器,允许你在最终数据库执行 SQL 之前继续添加条件约束到关联查询上。

例如,假定在一个博客系统中一个 User 模型有很多相关的 Post 模型:

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

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

你可以像这样查询 posts 关联并添加额外的条件约束到该关联关系上:

$user = App\User::find(1);
$user->posts()->where('active', 1)->get();

你可以在关联关系上使用任何查询构建器提供任何的方法!所以,掌握查询构建器的使用是掌握所有 Laravel 数据库操作的重要基石。

在关联查询后链接 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\User::find(1);

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

动态属性是“懒惰式加载”,意味着当你真正访问它们的时候才会加载关联数据。正因为如此,开发者经常使用渴求式加载来预加载他们知道在加载模型时要被访问的关联关系。渴求式加载有效减少了必须要被执行用以加载模型关联的 SQL 查询。

查询存在的关联关系

访问一个模型的记录的时候,你可能希望基于关联关系是否存在来限制查询结果的数目。例如,假设你想要获取所有至少有一个评论的博客文章,要实现这个功能,可以传递关联关系的名称到 hasorHas 方法:

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

你还可以指定操作符和数目来自定义查询:

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

还可以使用”.“来构造嵌套 has 语句,例如,你要获取所有至少有一条评论及投票的文章:

// 获取所有至少有一条评论获得投票的文章...
$posts = Post::has('comments.votes')->get();

如果你需要更强大的功能,可以使用 whereHasorWhereHas 方法将「where」条件放到 has 查询上,这些方法允许你添加自定义条件约束到关联关系条件约束,例如检查一条评论的内容:

use Illuminate\Database\Eloquent\Builder;

// Retrieve posts with at least one comment containing words like foo%...
$posts = App\Post::whereHas('comments', function ($query) {
    $query->where('content', 'like', 'foo%');
})->get();

// Retrieve posts with at least ten comments containing words like foo%...
$posts = App\Post::whereHas('comments', function ($query) {
    $query->where('content', 'like', 'foo%');
}, '>=', 10)->get();

无关联结果查询

访问一个模型的记录时,你可能需要基于缺失关联关系的模型对查询结果进行限定。例如,假设你想要获取所有没有评论的博客文章,可以传递关联关系名称到 doesntHaveorDoesntHave 方法来实现:

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

如果你需要更多功能,可以使用 whereDoesntHaveorWhereDoesntHave 方法添加更多「where」条件到 doesntHave 查询,这些方法允许你添加自定义约束条件到关联关系约束,例如检查评论内容:

use Illuminate\Database\Eloquent\Builder;

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

还可以使用「.」号查询嵌套的关联关系,例如,下面的查询会从有效作者那里获取所有带评论的文章:

use Illuminate\Database\Eloquent\Builder;

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

多态关联查询

为了查询 MorphTo 关联关系是否存在,你可以使用 whereHasMorph 方法和对应的关联方法:

use Illuminate\Database\Eloquent\Builder;

// Retrieve comments associated to posts or videos with a title like foo%...
$comments = App\Comment::whereHasMorph(
    'commentable',
    ['App\Post', 'App\Video'],
    function (Builder $query) {
        $query->where('title', 'like', 'foo%');
    }
)->get();

// Retrieve comments associated to posts with a title not like foo%...
$comments = App\Comment::whereDoesntHaveMorph(
    'commentable',
    'App\Post',
    function (Builder $query) {
        $query->where('title', 'like', 'foo%');
    }
)->get();

你可以使用 $type 参数基于关联模型添加不同的约束:

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 会执行一次额外查询来完成这个操作:

use Illuminate\Database\Eloquent\Builder;

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

统计关联模型

如果你想要在不加载关联关系的情况下统计关联结果数目,可以使用 withCount 方法,该方法会放置一个 {relation}_count 字段到结果模型。例如:

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

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

你可以像添加约束条件到查询一样来添加多个关联关系的「计数」:

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

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

还可以为关联关系计数结果设置别名,从而允许在一个关联关系上进行多维度计数:

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

echo $posts[0]->comments_count;

echo $posts[0]->pending_comments_count;

如果你将 withCountselect 语句组合起来使用,需要在 select 方法之后调用 withCount

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

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

此外,使用 loadCount 方法,你可以在父模型获取之后加载关联关系统计:

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

$book->loadCount('genres');

如果你需要在渴求式加载上设置额外的查询约束,可以传递一个你希望加载的关联关系键数组。该数组值应该是一个接收查询构建器实例作为参数的闭包:

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

渴求式加载

当以属性方式访问 Eloquent 关联关系的时候,关联关系数据是「懒惰式加载」的,这意味着关联关系数据直到第一次访问的时候才被加载。不过,Eloquent 还可以在查询父级模型的同时「渴求式加载」关联关系。渴求式加载缓解 N+1 查询问题,要阐明 N+1 查询问题,查看关联到 AuthorBook 模型:

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

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

现在,让我们获取所有书及其作者:

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

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

该循环先执行 1 次查询获取表中的所有书,然后另一个查询获取每一本书的作者,因此,如果有25本书,要执行26次查询:1次是获取书本身,剩下的25次查询是为每一本书获取其作者。

谢天谢地,我们可以使用渴求式加载来减少该操作到 2 次查询。当查询的时候,可以使用 with 方法指定应该被渴求式加载的关联关系:

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

嵌套的渴求式加载

要渴求式加载嵌套的关联关系,可以使用”.“语法。例如,我们在一个 Eloquent 语句中渴求式加载所有书的作者及所有作者的个人联系方式:

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

嵌套渴求式加载 morphTo 关联关系

如果你想要渴求式加载 morphTo 关联关系,以及该关联关系可能返回的各种实体中包含的嵌套关联关系,可以结合使用 with 方法和 morphTo 关联关系的 morphWith 方法。我们可以通过下面这个模型来演示该方法的使用:

<?php

use Illuminate\Database\Eloquent\Model;

class ActivityFeed extends Model
{
    /**
     * Get the parent of the activity feed record.
     */
    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 允许你在关联查询时指定要查询的字段:

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

注:使用这个特性时,id 字段是必须列出的。

默认的渴求式加载

有时候你可能想要在获取某个模型时总是加载一些关联关系。要实现这个功能,可以在该模型中定义一个 $with 属性:

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Book extends Model
{
    /**
     * The relationships that should always be loaded.
     *
     * @var array
     */
    protected $with = ['author'];

    /**
     * Get the author that wrote the book.
     */
    public function author()
    {
        return $this->belongsTo('App\Author');
    }
}

如果你想要针对某个查询从 $with 属性中移除某个项,可以使用 without 方法:

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

带条件约束的渴求式加载

有时候我们希望渴求式加载一个关联关系,但还想为渴求式加载指定更多的查询条件:

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

在这个例子中,Eloquent 只渴求式加载 title 包含 first 的文章。当然,你还可以调用其它查询构建器来自定义渴求式加载操作:

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

注:查询构建器方法 limittake 不能在渴求式加载中使用。

懒惰渴求式加载

有时候你需要在父模型已经被获取后渴求式加载一个关联关系。例如,这在你需要动态决定是否加载关联模型时可能很有用:

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

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

如果你需要设置更多的查询条件到渴求式加载查询上,可以传递一个包含你想要记载的关联关系数组到 load 方法,数组的值应该是接收查询实例的闭包:

$books->load(['author' => 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
{
    /**
     * Get the parent of the activity feed record.
     */
    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'],
    ]);

插入 & 更新关联模型

save 方法

Eloquent 为添加新模型到关联关系提供了便捷方法。例如,如果你需要插入新的 CommentPost 模型,可以从关联关系的 save 方法直接插入 Comment 而不是手动设置 Commentpost_id 属性:

$comment = new App\Comment(['message' => 'A new comment.']);
$post = App\Post::find(1);
$post->comments()->save($comment);

注意我们没有用动态属性方式访问 comments,而是调用 comments 方法获取关联关系实例。save 方法会自动添加 post_id 值到新的Comment 模型。

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

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

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

递归保存模型&关联关系

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

$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 数组:

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

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

注:使用 create 方法之前确保先浏览属性批量赋值文档

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

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

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

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

从属关联关系

更新 belongsTo 关联的时候,可以使用 associate 方法,该方法会在子模型设置外键:

$account = App\Account::find(10);
$user->account()->associate($account);
$user->save();

移除 belongsTo 关联的时候,可以使用 dissociate 方法。该方法会设置关联关系的外键为 null

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

默认模型

belongsTo 关联关系允许你在给定关联关系为 null 的情况下定义一个默认的返回模型,我们将这种模式称之为空对象模式,使用这种模式的好处是不用在代码中编写大量的判断检查逻辑。在下面的例子中,user 关联将会在没有用户与文章关联的情况下返回一个空的 App\User 模型:

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

要通过属性填充默认的模型,可以传递数据或闭包到 withDefault 方法:

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

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

多对多关联

附加/分离

处理多对多关联的时候,Eloquent 还提供了一些额外的辅助函数使得处理关联模型变得更加方便。例如,我们假定一个用户可能有多个角色,同时一个角色属于多个用户,要通过在连接模型的中间表中插入记录附加角色到用户上,可以使用 attach 方法:

$user = App\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\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当前被附加,则取消附加,类似的,如果当前没有附加,则附加:

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

在中间表上保存额外数据

处理多对多关联时,save 方法接收额外中间表属性数组作为第二个参数:

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

更新中间表记录

如果你需要更新中间表中已存在的行,可以使用 updateExistingPivot 方法。该方法接收中间记录外键和属性数组进行更新:

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

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

触发父模型时间戳更新

当一个模型属于另外一个时,例如 Comment 属于 Post,子模型更新时父模型的时间戳也被更新将很有用,例如,当 Comment 模型被更新时,你可能想要”触发“更新其所属模型 Postupdated_at 时间戳。Eloquent 使得这项操作变得简单,只需要添加包含关联关系名称的 touches 属性到子模型即可:

<?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 模型缓存:

$comment = App\Comment::find(1);
$comment->text = 'Edit to this comment!';
$comment->save();

相关文章推荐:


Vote Vote Cancel Collect Collect Cancel

<< 上一篇: 快速入门

>> 下一篇: 集合