本篇翻译自 Laravel 官方文档。

前言

数据表经常互相关联。例如,一篇博客的文章有很多评论,或者一个订单关联到一个下单的用户。通过 Eloquent 可以很简单地管理和使用这些关联,并且 Eloquent 还提供了几种不同的关联类型:

关联定义

Eloquent 的关联定义为模型的一个方法。然后,像 Eloquent 模型一样,关联同样提供了强大的查询构造器 ,定义的关联方法提供了强大的链式引用以及查询能力。例如:

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

但是,在继续深入关联之前,让我们先来学习如何定义各种类型的关联:

一对一

一对一关联是最基础的关联。例如,一个 User 模型可能会关联于一个 Phone 模型。为了定义这个关联,我们添加了一个 phone 方法到 User 类。phone方法将返回 Eloquent 基类的 hasOne 方法的返回值:

    <?php

    namespace App;

    use Illuminate\Database\Eloquent\Model;

    class User extends Model
    {
        /**
         * 获取 User 关联的 Phone 模型
         */
        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。换句话说,Eloquent 将会使用 Userid 去匹配 Phoneuser_id 对应的记录。如果你不使用 id,你可以传入第三个参数到 hasOne 方法中来自定义你的键名:

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

定义反向的关联

到现在为止,我们可以通过 Userphone 动态属性访问其对应的 Phone 模型。现在我们在 Phone 模型上定义一个到 拥有该 PhoneUser 关联。可以使用 belongsTohasOne 方法来定义一个反向的关联:

    <?php

    namespace App;

    use Illuminate\Database\Eloquent\Model;

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

在上面的例子中,Eloquent 将尝试根据定义在 Phone 中的 user_idUser 对应的数据表中查找。Eloquent 通过给关联方法名加上 _id 后缀来确定使用的外键。然后,如果在 Phone 模型中的外键名不为 user_id ,你需要传入 belongsTo 方法的第二个参数来指定外键名:

    /**
     * 获取该 Phone 的拥有者 (User)
     */
    public function user()
    {
        return $this->belongsTo('App\User', 'foreign_key');
    }

如果当前的模型没有使用 id 作为它的主键,或者你希望用另外的键名来做关联,你可以传入 belongsTo 方法的第三个参数来进行自定义:

    /**
     * 获取该 Phone 的拥有者 (User)
     */
    public function user()
    {
        return $this->belongsTo('App\User', 'foreign_key', 'other_key');
    }

一对多

“一对多” 关联通常在定义一个模型拥有多个其他模型时使用。例如,一片博客文章可能很多的评论。像其他关联定义方法一样,一对多的关联也将定义为一个 Eloquent 模型的方法:

    <?php

    namespace App;

    use Illuminate\Database\Eloquent\Model;

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

请记住,Eloquent 将自动确定 Comment 模型的合适的外键。按照惯例,Eloquent 将使用本模型(Post)的 “snake case” 后的名称,并且在其后加上 _id。所以,在这种情况下,Eloquent 将假定 Comment 模型的外键为 post_id

一旦关联被定义,我们就可以通过 comments 动态属性来获取评论的集合。请记住,Eloquent 提供 “动态属性”,我们可以将定义的关联方法当作属性来使用:

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

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

当然,所有的关联都提供了查询构造器,你可以通过添加约束来进一步控制 comment 方法返回的评论,并且可以使用链式方法:

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

hasOne 方法一样,你可以通过传入额外的参数来覆盖默认的外键和主键:

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

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

定义反向关联

现在我们已经可以获取一篇博客文章的所有评论了,让我们再来定义一个关联,使得可以通过评论获取到其对应的文章。为了定义 hasMany 的反向关联,我们给评论模型加上了 belongsTo 方法:

    <?php

    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;

在上面的例子中,Eloquent 将使用 Comment 模型中的 post_id 字段来匹配 Post 模型的 id 字段。Eloquent 将默认将该关联的名称加上 _id结尾来当作外键使用。然而,如果 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');
    }

多对多

多对多关联将比 hasOnehasMany 略微复杂一些。例如,一个用户拥有不同的角色,角色也可能属于多个用户。一些用户拥有 Admin 角色,为了定义这种关联需要三张数据表:users, roles, role_userrole_user 表衍生于按字母顺序排列的模型名称,并且含有 user_idrole_id 字段。

多对多的关联定义是通过使用 belongsToMany 方法来完成的。例如,我们来定义 Userroles 方法:

    <?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', 'user_roles');

通过额外的参数,你还可以自定义中间表的字段名称。传递给 belongsToMany 方法的第三个参数将覆盖当前表的外键,第四个参数将覆盖关联表的外键:

    return $this->belongsToMany('App\Role', 'user_roles', '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 中的定义一样简单,像前面提到过的一样,此方法同样接受参数来自定义相关的键名,并且提供了同样的构造查询器以及链式方法。

获取中间表字段

像你刚刚学到的一样,多对多的关联需要一个中间表。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();

间接关联

间接关联通过中间关联来提供一个常用的快捷方式来访问相对较远的关联。例如,一个 Country 模型可能会通过 一个 User 模型来拥有许多 Post 模型。在这个例子中,你可以简单地收集一个指定的 Country 下所有的 Post。让我们来看看定义这种关联需要的数据表:

countries
    id - integer
    name - string

users
    id - integer
    country_id - integer
    name - string

posts
    id - integer
    user_id - integer
    title - string

尽管 post 表没有包含一个名为 country_id 的字段,但 hasManyThrough 方法提供了形如 $country->posts 的调用来访问到指定 CountryPost。Eloquent 使用中间表 users 中的 country_id 字段来完成此功能。在获取到对应 CountryUser 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 将应用上面所提到过的外键惯例。如果你希望自定义这些键名,你可以传入第三个参数来覆盖中间模型的外键名,第四个参数来覆盖最终模型的外键名。

    class Country extends Model
    {
        public function posts()
        {
            return $this->hasManyThrough('App\Post', 'App\User', 'country_id', 'user_id');
        }
    }

多态关联

数据表结构

多态关联允许一个模型在同一个关联中对应多个模型。例如,想象你想要同事存储你的同事(staff)和你的所有产品(products)的照片。通过使用多态关联,你可以使用一个 photos 表就完成所有的功能。首先,让我们来确认实现此关联所需的数据表结构:

staff
    id - integer
    name - string

products
    id - integer
    price - integer

photos
    id - integer
    path - string
    imageable_id - integer
    imageable_type - string

photos 表中最重要的就是 imageable_idimagealbe_type 两个字段。 imageable_id 字段将保存你的同事或者产品所对应的ID,imageable_type 字段将保存其各自对应的模型类名。imageable_type 字段将决定 ORM 系统使用哪个模型来完成 imageable 关联的查询。

模型结构

接下来,我们来完成模型结构的定义:

    <?php

    namespace App;

    use Illuminate\Database\Eloquent\Model;

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

    class Staff extends Model
    {
        /**
         * Get all of the staff member's photos.
         */
        public function photos()
        {
            return $this->morphMany('App\Photo', 'imageable');
        }
    }

    class Product extends Model
    {
        /**
         * Get all of the product's photos.
         */
        public function photos()
        {
            return $this->morphMany('App\Photo', 'imageable');
        }
    }

调用多态关联

在数据表和模型定义完成以后,可以开始通过模型来访问关联。例如,要获取一个同事的所有照片,我们可以简单地使用 photos 动态属性:

    $staff = App\Staff::find(1);

    foreach ($staff->photos as $photo) {
        //
    }

你也可以通过访问定义了 morphoTo 方法的动态属性来访问对应的多态模型。在这个例子中,就是 Photo 模型的 imageable 方法。所以我们可以这样来访问动态属性:

    $photo = App\Photo::find(1);

    $imageable = $photo->imageable;

imageable 关联返回 StaffProduct 实例,取决于何种类型拥有该 Photo

多对多多态关联

数据表结构

除了定义传统的多态关联之外,你还可以定义 “多对多” 多态关联。例如,PostVideo 模型将共同拥有一个多态关联,关联到 Tag 模型。使用多对多多态关联,可以返回一个 PostVideos 都共同拥有的单独的唯一的列表。首先,我们来看表结构:

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 基类的 morghToMany 方法:

    <?php

    namespace App;

    use Illuminate\Database\Eloquent\Model;

    class Post extends Model
    {
        /**
         * Get all of the tags for the post.
         */
        public function tags()
        {
            return $this->morphToMany('App\Tag', 'taggable');
        }
    }

定义反向关联

接下来,在 Tag 模型中,你可以为不同的关联模型定义不同的关联方法。所以,在这个例子中,我们将定义 post 方法和 videos 方法:

    <?php

    namespace App;

    use Illuminate\Database\Eloquent\Model;

    class Tag extends Model
    {
        /**
         * Get all of the posts that are assigned this tag.
         */
        public function posts()
        {
            return $this->morphedByMany('App\Post', 'taggable');
        }

        /**
         * Get all of the videos that are assigned this tag.
         */
        public function videos()
        {
            return $this->morphedByMany('App\Video', 'taggable');
        }
    }

获取关联

在数据表和模型都定义完成后,就可以通过模型来访问关联。例如,要获取一个 post 的所有的 tags,可以简单地使用 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 模型:

    <?php

    namespace App;

    use Illuminate\Database\Eloquent\Model;

    class User extends Model
    {
        /**
         * Get all of the posts for the user.
         */
        public function posts()
        {
            return $this->hasMany('App\Post');
        }
    }

你可以通过增加额外的约束来调用 posts 关联:

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

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

请注意,你可以在关联方法上使用所有查询构造器(query builder)中的方法!

关联方法 vs 动态属性

如果你不需要添加额外的约束来访问关联查询,那么你可以简单地使用动态属性。例如,继续我们刚才的 UserPost 例子,我们将可以这样来获取所有的 usersposts

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

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

动态属性是“按需加载”的,意味着它们将在你调用时才执行。因为如此,开发者经常使用预加载来预先加载那些加载完模型后还需要继续加载的关联。预加载提供了一个有意义的 SQL 查询简化来执行模型的预加载。

关联查询方式

在获取一个模型的记录时,你可能希望通过一个关联是否存在来过滤返回的结果。例如,想象你想要获取所有至少有一条评论的文章。使用 has 方法并传入关联的名称便可实现:

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

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

    // 得到所有评论数大于等于3的文章
    $posts = Post::has('comments', '>=', 3)->get();

嵌套的 has 语句可以使用 “.” 符号。例如,你可以获取所有至少有一条评论和一个投票的文章:

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

如果你需要更强大的功能,你可以使用 whereHasorWhereHas 方法来增加 “where” 条件到你的 has 查询上。这些方法允许你添加自定义的约束到一个关联,比如检查一条评论的内容:

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

预加载

把 Eloquent 的关联当作属性来使用时,关联的数据是“按需加载”的。意味着关联的数据在你访问它之前并没有被真正加载。然而,Eloquent 可以在查询模型的时候 “预加载” 模型的关联。预加载解决了 N + 1 查询问题。为了说明 N + 1 查询问题,想象一个 Book 模型关联到 Author 模型:

    <?php

    namespace App;

    use Illuminate\Database\Eloquent\Model;

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

现在我们来获取所有的 Book 和它们的 Author

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

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

这个循环将执行一个查询来获取所有的 Book,然后将为每个 Book 模型执行一次 Author 的查询。所以,如果我们拥有25本书,那么这个循环将执行26此查询:第一次查询所有的书,剩下的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();

约束预加载

有时你可能希望预加载一个关联,但是同样希望指定约束来进行关联查询。例如:

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

    }])->get();

在这个例子中,Eloquent 将仅预加载那些文章标题中包含 first 字样的文章。当然,你也可以调用[查询构造器]的任意方法来自定义预加载的操作:

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

    }])->get();

按需预加载

有时你可能需要在获取一个模型的记录之后再预加载它的一个关联。例如,在你需要动态决定是否预加载的时候很有用:

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

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

如果你需要为预加载设置额外的查询约束,你可以传入一个闭包函数 Closureload 方法:

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

插入关联模型

保存方法

Eloquent 提供来一些方法的方法来添加新的记录到一个关联。例如,假定你需要为 Post 模型插入一条新的 Comment。你可以使用关联的 save 方法来插入 Comment,而不是手动设置它的 post_id

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

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

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

请注意,我们没有像访问动态属性一样访问 comment 关联,而是调用了 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 方法 和 多对多关联

当使用多对多关联时,save 方法支持使用一个数组来插入中间表中的额外数据:

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

Create 方法

除了 savesaveMany 方法,你还可以使用 create 方法,它将支持传入一个属性数组,创建一个模型,然后将它插入到数据库中。并且,savecreate 方法的区别是 save 方法支持 Eloquent 模型,而 create 支持原生 PHP 的数组:

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

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

在使用 create 方法之前,确保已经回顾过一次 批量赋值 的文档。

更新 “Belongs To” 关联

当更新一个 belongsTo 定义的关联时,你可以使用 associate 方法。这个方法将自动设置关联的外键:

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

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

    $user->save();

当删除一个 belongsTo 的关联时,可以使用 dissociate 方法。这个方法将重置关联的外键:

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

    $user->save();

多对多关联

Attaching / Detaching

在使用多对多关联时,Eloquent 提供了几个额外的帮助方法,使得构造模型之间的关联变得简单。例如,一个用户可以拥有多个角色,一个角色也可以拥有多个用户。可以使用 attach 方法来将一条记录插入中间表,以达到将一个角色附到一个用户之上:

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

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

将一个关联附到一个模型时,可以通过传入一个额外的数组来向中间表插入额外的数据:

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

当然,有时从一个用户删除一个角色也是很有必要的。可以使用 detach 方法来实现。detach 方法将删除合适的中间表记录,然而,所有的模型在数据库中保持不变:

    // Detach a single role from the user...
    $user->roles()->detach($roleId);

    // Detach all roles from the user...
    $user->roles()->detach();

为了更简单,attachdetach 方法也接受一个包含ID的数组:

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

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

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

简便地同步

你也可以使用 sync 方法来构造多对多关联。sync 方法允许传入一个包含 ID 的数组来插入到中间表中。所有不存在于给定数组的 ID 将从中间表删除。所以,此操作完成时,只有给定数组中包含的 ID 才会存在于中间表中:

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

你也可以将额外数据作为 ID 对应的项来更新中间表中的其他字段:

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

更新父级的时间戳

当一个模型通过 belongsTobelongsToMany 方法关联于其他模型时,如一个 Comment 模型 belongsTo 一个 Post 模型,有时候也需要在更新关联时同时更新关联的父级时间戳。例如,当一个 Comment 模型被更新,你若想要自动更新 Postupdated_at 时间戳的话,Eloquent 提供了简单的方法。仅仅只需在子级模型中添加一个名为 touches 的属性,其中包含了关联的名称:

    <?php

    namespace App;

    use Illuminate\Database\Eloquent\Model;

    class Comment extends Model
    {
        /**
         * All of the relationships to be touched.
         *
         * @var array
         */
        protected $touches = ['post'];

        /**
         * Get the post that the comment belongs to.
         */
        public function post()
        {
            return $this->belongsTo('App\Post');
        }
    }

现在,当一个 Comment 被更新时,父级 Post 也将自动同步更新它的 updated_at 字段:

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

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

    $comment->save();