Eloquent:关联

简介

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

定义关联

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

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

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

一对一

file

一对一关联是非常基础的关系。例如,一个 User 模型可能被关联到一个 Phone。要定义此关联,可以在 User 模型上添加一个 phone 方法。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 外键。如果要覆盖此约定,可以传递第二个参数给 hasOne 方法:

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

此外,Eloquent 假定外键有一个值与父模型的 id(或其它自定义 $primaryKey)字段相匹配。换句话说,Eloquent 会在 Phone 记录的 user_id 字段中查找用户的 id 字段的值。如果要在关联时使用 id 以外的值,可以传递第三个参数给 hasOne 方法指定自定义键:

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

定义相对的关联

现在,我们可以从 User 获取 Phone 模型了。接下来,我们在 Phone 模型上定义一个关联,获取拥有此电话的 User。可以使用 belongsTo 方法定义与 hasOne 相对的关联:

namespace App;

use Illuminate\Database\Eloquent\Model;

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

在上述示例中,Eloquent 会尝试在 User 模型的 id 上查找和 user_id 相匹配的值。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');
}

一对多

file

「一对多」关联用于定义单个模型拥有任何数量的其它模型。例如,一篇博客文章可能有任意数量的评论。与所有其它 Eloquent 关联一样,通过在模型上添加一个函数定义一对多关联:

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 获取评论时继续在关联上链式添加其它约束:

$comment = 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');

一对多(反向)

既然可以获取文章的所有评论了,接着我们再定义一个关联允许评论获取其所属的文章。要定义 hasMany 相对的关联,可以在子模型上定义一个关联函数,调用 belongsTo 方法:

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 会尝试在 Post 模型的 id 上查找和 Commentpost_id 相匹配的值。Eloquent 通过检查关联方法名并添加 _ 后缀,再跟上主键名决定默认的外键名。当然,如果 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');
}

多对多

file

多对多关联比 hasOnehasMany 关系稍微复杂一点。一个示例是,一个用户有多个角色,而角色也被其他用户共享。例如,很多用户都有「管理员」角色。要定义这种关联,需要三张数据表: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 会按照字母顺序连接两个关联模型的名称。当然,您可以通过传递第二个参数给 belongsToMany 方法覆盖此约定:

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 属性访问中间表属性。当然,您可以自由定义此属性名来更好地反映其在应用中的用途。

例如,如果应用中包含可能订阅播客的用户,那么用户和播客之间可能就是多对多关联。这种情况下,可能希望将中间表 pivot 属性重命名为 subscription。可以在定义关联时使用 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 类。例如,我们定义一个使用自定义 UserRole 中间表模型的 Role

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
{
    //
}

远程一对多

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 会在中间表 users 上检查 country_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' // 用户表的本地键
        );
    }
}

多态关联

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_type 字段。commentable_id 字段会包含文章或视频的 ID,而 commentable_type 字段会包含父模型的类名。commentable_type 字段会在 ORM 获取 commentable 关联时用于确定返回哪个「类型」的父模型。

模型结构

接下来,我们查看创建此关联所需的模型定义:

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 动态属性:

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

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

还可以通过执行调用了 morphTo 的方法名在多态模型中获取多态关联的拥有者。在我们的示例中,就是 Comment 模型上的 commentable 方法。因此,我们可以像动态属性一样访问此方法:

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

$commentable = $comment->commentable;

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

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

默认情况下,Laravel 会使用完整的类名存储关联模型的类型。举例说明,上述示例中 Comment 可能属于一个 PostVideo,因此默认的 commentable_type 会分别是 App\PostApp\Video。但是,您可能希望将数据库从应用的内部结构中解耦。这种情况下,可以定义一个关联的「多态映射」指示 Eloquent 为每个模型使用自定义名称而不是类名:

use Illuminate\Database\Eloquent\Relations\Relation;

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

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

多对多多态

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

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) {
    //
}

查询关联

由于所有类型的 Eloquent 关联都通过方法定义,因此可以调用这些方法获取一个关联实例,而不用实际执行关联查询。此外,所有类型的 Eloquent 关联都可以作为 查询构造器 使用,所以可以在数据库最终运行 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();

可以在关联上使用任何 查询构造器 方法,因此务必查看查询构造器的文档了解有哪些可用的方法。

关联方法 Vs. 动态属性

如果不需要对 Eloquent 关联查询添加额外约束,可以获取关联就像它们是属性一样。例如,继续使用 UserPost 模型为例,我们可以像这样获取用户的所有文章:

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

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

动态属性是「懒加载」,意味着只有在实际访问时它们才会加载其关联数据。因此,开发者经常使用「预加载」在加载模型后提前加载要使用的关联。预加载有效减少了加载模型关联必须执行的 SQL 查询。

查询已存在关联

获取模型记录时,可能希望基于已存在的关联对结果进行限制。例如,假设希望获取至少有一条评论的所有博客文章。要完成此操作,可以传递关联名称给 hasorHas 方法:

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

也可以指定操作符和数量进行更详细的自定义查询:

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

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

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

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

// 获取至少有一条以 foo 起头的评论的所有文章
$posts = App\Post::whereHas('comments', function ($query) {
    $query->where('content', 'like', 'foo%');
})->get();

查询不存在关联

获取模型记录时,可能希望基于不存在的关联对结果进行限制。例如,假设希望获取没有任何评论的所有博客文章。要完成此操作,可以传递关联名称给 doesntHaveorDoesntHave 方法:

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

如果需要更强大的用法,可以使用 whereDoesntHaveorWhereDoesntHave 方法将「where」条件添加到 doesntHave 查询。这些方法允许您将自定义约束添加到关联约束,例如检查一条评论的内容:

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

还可以使用「点」语法查询嵌套的关联。例如,下列查询会获取评论作者没有被禁用的所有文章:

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

关联模型计数

如果要在不实际加载关联结果的情况下统计其数量,可以使用 withCount 方法,它会在结果模型中放一个 {relation}_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;

预加载

像属性一样获取 Eloquent 关联时,关联数据是「懒加载」。这意味着关联数据只会在初次访问属性时才会实际加载。但是,在查询父模型时 Eloquent 可以同时「预加载」关联。预加载避免了 N+1 查询问题。为了说明 N+1 查询问题,假设 Book 模型关联到了 Author

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

在此操作中,只会执行 2 次查询:

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

预加载指定字段

获取关联时可能不总需要所有字段。因此,Eloquent 允许您指定要获取的关联字段:

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

使用此功能时,应该在要获取的字段列表中始终包括 id 字段。

约束预加载

有时可能希望预加载关联,但也希望为预加载查询指定其它查询约束。下面是一个示例:

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

延迟预加载

有时在获取父模型后可能需要预加载关联模型。例如,在需要动态决定是否加载关联模型时很有用:

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

插入 & 更新关联模型

save 方法

Eloquent 为添加新模型到关联中提供了便捷的方法。例如,可能需要为 Post 模型插入一个新的 Comment。可以直接使用关联的 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 方法 在关联上创建和更新模型

Belongs To 关联

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

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

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

$user->save();

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

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

$user->save();

默认模型

belongsTo 关联允许您定义一个当给定关联为 null 时返回的默认模型。这种模式通常被称为 空对象模式,可以帮助在代码中省去条件判断。在以下示例中,如果没有 user 附加到文章则 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);

更新父模型时间戳

当一个模型 belongsTobelongsToMany 另一个模型时(例如一个 Comment 属于一个 Post),在子模型更新时更新父模型的时间戳有时很有用。例如,Comment 模型更新后,您可能想要自动「触发」拥有者 Post 的时间戳更新。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 时,拥有者 Post 也会更新其 updated_at 字段,可以更方便地知道何时应该让 Post 模型的缓存失效:

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

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

$comment->save();