1. 简介

数据表之间通常有一定的关联。比如,一篇博客文章可能有多条评论,或者一个订单对应一个下单用户。Eloquent 使得我们更容易管理和使用这些关联,下面是支持的几种不同的关联类型:

  • 一对一
  • 一对多
  • 多对多
  • 远程一对多
  • 多态一对多
  • 多态多对多

2. 定义关联

在 Eloquent 模型类中用方法来定义 Eloquent 关联。因此,和 Eloquent 模型自身一样,关联也可以作为强大的查询构造器使用,这也就提供了强大的链式调用和查询功能。例如,我们可以在 posts 关联的链式调用中加上约束条件:

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

但是,在深入使用关联之前,我们先来看看如何定义每种关联类型。

2.1 一对一

file

一对一是最基本的关系。例如,一个 User 模型可能关联一个 Phone 模型。定义该关联,我们要在 User 模型中添加一个 phone 方法,在里面调用 hasOne 方法并返回其结果:

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。如果要使用其它外键名,可以通过传递第二个参数指定:

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

此外,Eloquent 默认会在 Phone 记录的 user_id 列中查找和用户表的 id(或自定义 $primaryKey)相匹配的值。如果不使用 id,可以通过传递第三个参数指定其它键名:

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

一对一反向关联

现在,我们已经能从 User 访问 Phone 模型了。接下来,我们在 Phone 上也定义一个关联,来访问拥有该电话的 User 模型。这里使用与 hasOne 对应的 belongsTo 方法来定义相对的关联:

namespace App;

use Illuminate\Database\Eloquent\Model;

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

上面的例子中,Eloquent 会在 User 记录的 id 列中查找和 user_id 相匹配的值。如果 Phone 模型的外键不是 user_id,可以通过第二个参数指定其它键名:

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

如果 User 模型没有使用 id 作为主键,或者希望使用其它字段,也可以通过传递第三个参数指定键名:

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

2.2 一对多

file

一对多用来定义一个模型拥有多个的其它关联模型。例如,一篇博客文章可能会有多条评论。和其它关联一样,定义一对多关联也是在 Eloquent 模型中添加一个方法:

namespace App;

use Illuminate\Database\Eloquent\Model;

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

和前面一样,默认情况下,Eloquent 会使用父模型名的「蛇形命名」加上 _id 后缀作为外键字段。这里,Comment 模型对应到 Post 模型上的外键字段是 post_id

定义好关联之后,我们就可以通过 Eloquent 提供的动态属性 comments 来访问评论的集合了:

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

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

当然,由于所有的关联还可以作为查询构造器使用,还可以使用链式调用的方式,在 comments 方法后面添加更多的约束条件:

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

hasOne 方法一样,也可以通过传递参数来覆盖默认的外键名和本地键名:

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

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

一对多反向关联

既然我们已经能获得一篇文章的所有评论,让我们接着再定义一个关联,来通过评论获得父模型文章。在子模型中使用 belongsTo 方法定义它:

namespace App;

use Illuminate\Database\Eloquent\Model;

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

定义好关联,就可以在 Comment 模型上使用动态属性 post 来获取 Post 模型了。

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

echo $comment->post->title;

如果 Comment 模型的外键不是 post_id,那么可以将自定义键名通过第二个参数传递给 belongsTo 方法:

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

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

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

2.3 多对多

file

多对多比一对一和一对多稍微复杂一点。举个例子,一个用户拥有多种角色,同时这些角色也被其他用户共享。例如,许多用户都可以有「管理员」角色。要定义这种关联,需要用到三张数据表:usersrolesrole_userrole_user 表的命名是由关联的两个模型名按照字母顺序而来的,并且包含了 user_idrole_id 字段。

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

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 会按照字母顺序连接两个关联模型的名称。当然,也可以通过传递第二个参数来指定表名:

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

除了指定中间表的表名外,还可以通过给 belongsToMany 方法传递其它参数来指定中间表的键名。第三个参数用来指定关联的模型在中间表里的外键名,第四个参数是另一个模型在中间表里的外键名:

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

多对多反向关联

要定义多对多的反向关联,只需要在对方模型里面再次调用 belongsToMany 方法就可以了。我们继续在 Role 模型里添加 users 方法:

namespace App;

use Illuminate\Database\Eloquent\Model;

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

如你所见,除了引入的模型变为 App\User 外,其它与在 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 对象只包含两个关键模型的键。如果中间表里还有额外字段,必须在定义关联时明确指出:

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

如果想让中间表自动维护 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 类。例如,我们在定义 Role 模型的关联时,使用自定义中间表模型 UserRole

namespace App;

use Illuminate\Database\Eloquent\Model;

class Role extends Model
{
    /**
     * 获得拥有该角色的用户
     */
    public function users()
    {
        return $this->belongsToMany('App\User')->using('App\UserRole');
    }
}

UserRole 模型继承自 Pivot 类:

namespace App;

use Illuminate\Database\Eloquent\Relations\Pivot;

class UserRole extends Pivot
{
    //
}

2.4 远程一对多

file

远程一对多提供了方便、简短的方式,通过中间表来获得远程的关联。例如,一个 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 模型中定义它:

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', // 用户表外键...
            'user_id', // 文章表外键...
            'id', // 国家表本地键...
            'id' // 用户表本地键...
        );
    }
}

2.5 多态一对多

file

数据表结构

多态关联允许一个模型属于多个其它模型。例如,用户可以评论文章和视频。使用多态关联,可以使用一张 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 用来保存文章或者视频的 ID 值,而 commentable_type 用来保存所属模型的类名。commentable_type 用来在访问 commentable 关联时,让 ORM 知道父模型具体是哪个类。

模型结构

接下来,我们看看建立该关联所需的模型定义:

namespace App;

use Illuminate\Database\Eloquent\Model;

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

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

获取多态关联

我们只需要简单地使用 comments 动态属性,就可以获取文章对应的所有评论:

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

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

也可以在多态模型上,通过访问调用了 morphTo 的关联方法获得多态关联的拥有者。在当前场景中,就是 Comment 模型的 commentable 方法。使用动态属性来访问:

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

$commentable = $comment->commentable;

Comment 模型的 commentable 关联会返回 Post 或者 Video 实例,具体取决于评论所属的模型类型。

自定义多态关联的类型字段

默认情况下,Laravel 会将关联模型的完全限定类名保存为关联模型类型。比如,在上例中,Comment 属于 Post 或者 Video,那么 commentable_type 的默认值分别对应的就是 App\PostApp\Video。如果希望将数据库与程序内部结构解耦,可以自定义一个「多态映射表」来指定 Eloquent 使用的字段名而不是类名:

use Illuminate\Database\Eloquent\Relations\Relation;

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

可以在 AppServiceProvider 中的 boot 函数中使用 Relation:morphMap 方法注册「多态映射表」,或者使用一个单独的服务提供者进行注册。

2.6 多态多对多

file

数据表结构

除了传统的多态关联,也可以定义多对多的多态关联。例如,Post 模型和 Video 模型可以共享一个和 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 方法:

namespace App;

use Illuminate\Database\Eloquent\Model;

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

多态多对多反向关联

下一步,在 Tag 模型中,为每个关联模型定义一个方法。所以,在本例中,我们要定义一个 posts 和一个 videos 方法:

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 模型上的 postsvideos 方法。所以,我们可以使用动态属性来访问这两个方法:

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

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

2. 查询关联

由于所有类型的关联都通过方法定义,你可以调用这些方法来获取关联实例,而不需要实际运行关联查询。此外,所有类型的关联都可以作为查询构造器使用,即可以在向数据库执行 SQL 语句前,使用链式调用添加约束条件。

例如,在一个博客系统,User 模型有多个关联的 Post 模型:

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

在关联上可以使用查询构造器的任意方法。

2.1 关联方法 Vs. 动态属性

如果不需要给 Eloquent 关联查询添加额外的约束条件,可以像访问属性一样访问关联。例如,在刚刚的 UserPost 模型例子中,我们可以这样访问一个用户的所有文章:

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

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

动态属性是「懒加载」的,意味着只有在被实际访问时才加载关联数据。因此,开发者经常使用「预加载」提前加载之后会用到的关联数据。「预加载」有效地减少了 SQL 请求数,避免了重复执行一个模型加载关联数据、发送 SQL 请求带来的性能问题。

2.2 基于已存在的关联进行查询

当获取模型记录时,可能希望基于存在的关联对结果进行约束。例如,想获得至少有一条评论的所有博客文章。为了实现该功能,可以给 hasorHas 方法传递关联名称:

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

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

// 获得有三条或三条以上评论的所有文章
$posts = Post::has('comments', '>=', 3)->get();

还可以使用「点」语法构造嵌套的 has 语句。例如,获得至少有一条获赞评论的所有文章:

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

如果需要更高级的用法,可以使用 whereHasorWhereHas 方法指定查询条件。例如对评论内容进行检查:

// 获得至少有一条评论内容满足 foo% 条件的所有文章
$posts = Post::whereHas('comments', function ($query) {
    $query->where('content', 'like', 'foo%');
})->get();

2.3 基于不存在的关联进行查询

当获取模型记录时,也可能希望基于不存在的关联对结果进行约束。例如,要获得没有任何评论的所有博客文章。为了实现该功能,可以给 doesntHaveorDoesntHave 方法传递关联名称:

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

如果需要更高级的用法,可以使用 whereDoesntHave 或者 orWhereDoesntHave 方法指定查询条件。例如对评论内容进行检查:

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

还可以使用「点」语法构造嵌套的 doesntHave 语句。例如,获得包含未禁用用户评论的所有文章:

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

2.4 关联计数

如果只是想统计结果集的数量而不需要这些数据时,可以使用 withCount 方法,它会在结果集模型中添加一个 {关联名}_count 字段。例如:

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

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

可以同时获取多个关联计数,并为其添加查询约束条件:

$posts = App\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_count' => function ($query) {
        $query->where('approved', false);
    }
])->get();

echo $posts[0]->comments_count;

echo $posts[0]->pending_comments_count;

3. 预加载

当用属性访问模型关联时,关联的数据是「懒加载」的。意味着关联数据只会在初次访问该属性时才被加载。在查询父模型时,Eloquent 可以「预加载」关联数据。预加载避免了 N+1 次查询的问题。以 BookAuthor 模型关联为例,说明 N+1 查询的问题:

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

这个循环将会执行一条语句去数据表查询所有的书籍信息,然后每本书执行另一条语句去获取作者信息。因此,如果我们有 25 本书,这个循环就会执行 26 条查询语句,一条获取书籍信息,另外 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, ...);

3.1 预加载多个关联

有时候,可能需要在一次操作中预加载多个不同的关联。只需要给 with 方法传递额外的参数就能实现:

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

3.2 嵌套预加载

可以使用「点」语法来嵌套预加载。例如,在一个 Eloquent 声明中,预加载所有书籍的作者和这些作者的个人联系信息:

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

3.3 预加载指定列

可能不总是需要从关联中获取每一列数据,为此,Eloquent 也允许在关联中指定要查询的列:

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

使用该功能时,需要在获取的列中始终包含 id 列。

3.4 约束预加载

在使用预加载时,有时需要进行额外的约束。如下:

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

3.5 延迟预加载

有时候,需要在已查询出来的模型上进行预加载。这在动态决定是否进行预加载时很有帮助:

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

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

如果需要在预加载上添加额外的约束,可以传入一个数组,关联作为数组的键,值是一个接收查询实例的闭包:

$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
    ];
}

4. 插入 & 更新关联模型

4.1 保存

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

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

递归保存模型和模型关联

如果要保存模型以及所有的模型关联,可以使用 push 方法:

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

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

$post->push();

4.2 新增

除了 savesaveMany 方法外,还可以使用接收属性数组的 create 方法。它会创建模型,并插入到数据库中。savecreate 方法的区别在于,save 方法接收一个完整的 Eloquent 模型实例,而 create 则接收一个数组:

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

4.3 更新 belongsTo 关联

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

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

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

$user->save();

当移除 belongsTo 关联时,可以使用 dissociate 方法。该方法会将关联外键设置为 null,但需要确保数据表中该字段可以为空:

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

$user->save();

注意此处的 belongsTo 关联,与关联类的类名是一一对应的。例如一个用户拥有多篇博客文章,dd($post->user()); 看到该关联是一个 belongsTo 类的实例,那么可以调用 associatedissociate 方法将博客文章对象关联到用户,或者移除对应关联。

$user = Auth::user();
$post = Post::first();

$post->user()->associate($user);
$post->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';
    });
}

4.4 多对多关联

附加 / 分离

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 已被附加在中间表中,那么它将会被移除。同样,如果给定的 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);

5. 更新父模型时间戳

当一个模型 belongsTo 或者 belongToMany 另一个模型,比如 Comment 属于 Post,在子模型更新的同时更新父模型的时间戳时非常有用。例如,当 Comment 模型被更新时,自动「触发」父模型 Postupdated_at 时间戳的更新。在 Eloquent 中很容易实现。只要在子模型添加一个包含关联名称的 touches 属性即可。

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 时,对应父模型 Postupdated_at 字段也会被同时更新。可以更方便地知道在什么时候该让一个 Post 模型的缓存失效:

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

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

$comment->save();