# 实体关联

# 简介

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

# 定义关联

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

$user->groups()->where('status', 1)->select();

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

# 一对一

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

<?php

class UsersEntity extends Entity
{
    //每个用户都有对应的属性
    public function profile()
    {
        return $this->hasOne(ProfileEntity::class);
    }
}

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

$profile = UsersEntity::find(1)->profile;

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

  return $this->hasOne(ProfileEntity::class, 'uid');

另外,Entity 假设外键的值是与父级主键列的值相匹配的。换句话说,Entity 将会在 ProfileEntity 记录的 uid 列中查找与用户表的 id 列相匹配的值。如果您希望该关联使用 id 以外的自定义键名,则可以给 hasOne 方法传递第三个参数:

return $this->hasOne(ProfileEntity::class, 'foreign_key', 'local_key');

# 定义反向关联

我们已经能从 UsersEntity 实体访问到 ProfileEntity 实体了。现在,让我们再在 ProfileEntity 实体上定义一个关联,这个关联能让我们访问到拥有该属性的 UsersEntity 实体。我们可以使用与 hasOne 方法对应的 belongsTo 方法来定义反向关联:

<?php

class ProfileEntity extends Entity
{
   //拥有此属性的用户
    public function user()
    {
        return $this->belongsTo(UsersEntity::class);
    }
}

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

//拥有此属性的用户
public function user()
{
    return $this->belongsTo(UsersEntity::class, 'uid');
}

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

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

# 一对多

『一对多』关联用于定义单个实体拥有任意数量的其它关联实体。例如,一个用户可能会有多条登录日志。正如其它所有的 Entity 关联一样,一对多关联的定义也是在 Entity 实体中写一个方法:


class UsersEntity extends Entity
{
    public function logs()
    {
        return $this->hasMany(LoginLogEntity::class, 'uid');
    }
}

记住一点,Entity 将会自动确定 LoginLogEntity 实体的外键属性。按照约定,Entity 将会使用所属实体名称的 『snake case』形式,再加上 _id 后缀作为外键字段。因此,在上面这个例子中,Entity 将假定 LoginLogEntity 对应到 UsersEntity 实体上的外键就是 users_id,在这里我们自定义了外键为uid

一旦关系被定义好以后,就可以通过访问 UsersEntity 实体的 logs 属性来获取登录日志的集合。记住,由于 Entity 提供了『动态属性』 ,所以我们可以像访问实体的属性一样访问关联方法:

$logs = UsersEntity::find(1)->logs;

foreach ($logs as $log) {
    //
}

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

$log = UsersEntity::find(1)->logs()->whereGt('ctime', strtotime('-7 days'))->select();

正如 hasOne 方法一样,你也可以在使用 hasMany 方法的时候,通过传递额外参数来覆盖默认使用的外键与本地键:

return $this->hasMany(LoginLogEntity::class, 'foreign_key');

return $this->hasMany(LoginLogEntity::class, 'foreign_key', 'local_key');

# 一对多 (反向)

现在,我们已经能获得一个用户的所有登录日志,接着再定义一个通过登录日志获得所属用户的关联关系。这个关联是 hasMany 关联的反向关联,需要在子级实体中使用 belongsTo 方法定义它:

<?php

class LoginLogEntity extends Entity
{
    //所属用户
    public function user()
    {
        return $this->belongsTo(UsersEntity::class, 'uid');
    }
}

这个关系定义好以后,我们就可以通过访问 LoginLogEntity 实体的 user 这个『动态属性』来获取关联的 UsersEntity 实体了:

$comment = LoginLogEntity::find(1);

echo $comment->user->username;

在上面的例子中,Entity 会尝试用 LoginLogEntity 实体的 uidUsersEntity 实体的 id 进行匹配。默认外键名是 Entity 依据关联名,并在关联名后加上 _再加上主键字段名作为后缀确定的。当然,如果 LoginLogEntity 实体的外键不是 users_id,那么可以将自定义键名作为第二个参数传递给 belongsTo 方法,如上uid

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

//所属用户
public function user()
{
    return $this->belongsTo(UsersEntity::class, 'uid', 'other_key');
}

# 多对多

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

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

<?php

class UsersEntity extends Entity
{
    /**
        * 用户拥有的角色
        */
    public function roles()
    {
        return $this->belongsToMany(RolesEntity::class);
    }
}

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

$user = UsersEntity::find(1);

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

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

$roles = web\Entity\UsersEntity::find(1)->roles()->orderBy('name')->select();

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

return $this->belongsToMany(RolesEntity::class, 'role_user');

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

return $this->belongsToMany(RolesEntity::class, 'role_user', 'users_id', 'role_id');

# 定义反向关联

要定义多对多的反向关联, 你只需要在关联实体中调用 belongsToMany 方法。我们在 RolesEntity 实体中定义 users 方法:

<?php


class RolesEntity extends Entity
{
   // 拥有此角色的用户
    public function users()
    {
        return $this->belongsToMany(UsersEntity::class);
    }
}

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

# 获取中间表字段

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

$user = web\Entity\UsersEntity::find(1);

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

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

# 自定义 pivot 属性名称

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

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

return $this->belongsToMany(PodcastEntity::class)h
                ->as('subscription');

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

$users = UsersEntity::with('podcasts')->select();

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

# 通过中间表过滤关系

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

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

return $this->belongsToMany('web\Entity\Role')->wherePivot('priority', [1, 2], 'in');

# 定义中间表实体

如果你想定义一个自定义实体来表示关联关系中的中间表,可以在定义关联时调用 using 方法。自定义多对多中间表实体都必须扩展自 Cml\Entity\Pivot 类。例如,我们在写 Role 实体的关联时,使用自定义中间表实体 RoleUser:

<?php

namespace web\Entity;

Cml\Entity\Entity;

class RoleEntity extends Entity
{
    /**
        * 拥有此角色的所有用户
        */
    public function users()
    {
        return $this->belongsToMany(UsersEntity::class)->using(RoleUsersEntity::class);
    }
}

当定义 RoleUser 实体时,我们要扩展 Pivot 类:

 <?php

namespace web\Entity;

use Cml\Entity\Pivot;

class RoleUsersEntity extends Pivot
{
    //
}

# 远程一对一关系

远程一对一关联通过一个中间关联实体实现。 例如,如果每个供应商都有一个用户,并且每个用户与一个用户历史记录相关联,那么供应商可以通过用户访问用户的历史记录,让我们看看定义这种关系所需的数据库表:

users
    id - integer
    supplier_id - integer

suppliers
    id - integer

history
    id - integer
    users_id - integer

虽然 history 表不包含 supplier_id ,但 hasOneThrough 关系可以提供对用户历史记录的访问,以访问供应商实体。现在我们已经检查了关系的表结构,让我们在 Supplier 实体上定义相应的方法:

<?php

namespace web\Entity;

Cml\Entity\Entity;

class SupplierEntity extends Entity
{
    /**
        * 用户的历史记录。
        */
    public function userHistory()
    {
        return $this->hasOneThrough('web\Entity\HistoryEntity', 'web\Entity\UsersEntity');
    }
}

传递给 hasOneThrough 方法的第一个参数是希望访问的实体名称,第二个参数是中间实体的名称。

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

class SupplierEntity extends Entity
{
    /**
        * 用户的历史记录。
        */
    public function userHistory()
    {
        return $this->hasOneThrough(
            'web\Entity\HistoryEntity',
            'web\Entity\UsersEntity',
            'supplier_id', // 用户表外键
            'users_id', // 历史记录表外键
            'id', // 供应商本地键
            'id' // 用户本地键
        );
    }
}

# 远程一对多关联

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

 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 访问到一个国家下所有的用户文章。为了完成这个查询,Entity 会先检查中间表 userscountry_id 字段,找到所有匹配的用户 ID 后,使用这些 ID,在 posts 表中完成查找。

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

<?php

namespace web\Entity;

Cml\Entity\Entity;

class CountryEntity extends Entity
{
    /**
     * 当前国家所有文章。
     */
    public function posts()
    {
        return $this->hasManyThrough('web\Entity\Post', 'web\Entity\UsersEntity');
    }
}

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

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

class CountryEntity extends Entity
{
    public function posts()
    {
        return $this->hasManyThrough(
            'web\Entity\Post',
            'web\Entity\UsersEntity',
            'country_id', // 国家表外键
            'user_id', // 用户表外键
            'id', // 国家表本地键
            'id' // 用户表本地键
        );
    }
}

# 多态关联

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

# 一对一 (多态)

# 表结构

一对一多态关联与简单的一对一关联类似;不过,目标实体能够在一个关联上从属于多个实体。例如,博客 PostEntityUsersEntity 可能共享一个关联到 ImageEntity 实体的关系。使用一对一多态关联允许使用一个唯一图片列表同时用于博客文章和用户账户。让我们先看看表结构:

  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 列包含的则是父实体的类名。Entity 在访问 imageable 时使用 imageable_type 列来判断父实体的 「类型」。

# 实体结构

接下来,再看看建立关联的实体定义:

<?php

namespace web\Entity;

Cml\Entity\Entity;

class ImageEntity extends Entity
{
    /**
     * 获取拥有此图片的实体。
     */
    public function imageable()
    {
        return $this->morphTo();
    }
}

class PostEntity extends Entity
{
    /**
     * 获取文章图片。
     */
    public function image()
    {
        return $this->morphOne('web\Entity\ImageEntity', 'imageable');
    }
}

class UsersEntity extends Entity
{
    /**
     * 获取用户图片。
     */
    public function image()
    {
        return $this->morphOne('web\Entity\ImageEntity', 'imageable');
    }
}

# 获取关联

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


$post = web\Entity\PostEntity::find(1);

$image = $post->image;

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

$image = web\Entity\ImageEntity::find(1);

$imageable = $image->imageable;

ImageEntity 实体的 imageable 关联将返回 PostEntityUsersEntity 实例,其结果取决于图片属性哪个实体。

# 一对多(多态)

# 表结构

一对多多态关联与简单的一对多关联类似;不过,目标实体可以在一个关联中从属于多个实体。假设应用中的用户可以同时 「评论」 文章和视频。使用多态关联,可以用单个 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 web\Entity;

Cml\Entity\Entity;

class CommentEntity extends Entity
{
    /**
     * 获取拥有此评论的实体。
     */
    public function commentable()
    {
        return $this->morphTo();
    }
}

class PostEntity extends Entity
{
    /**
     * 获取此文章的所有评论。
     */
    public function comments()
    {
        return $this->morphMany('web\Entity\CommentEntity', 'commentable');
    }
}

class VideoEntity extends Entity
{
    /**
     * 获取此视频的所有评论。
     */
    public function comments()
    {
        return $this->morphMany('web\Entity\CommentEntity', 'commentable');
    }
}

# 获取关联

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

$post = web\Entity\PostEntity::find(1);

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

还可以通过访问执行 morphTo 调用的方法名来从多态实体获取其所属实体。在本例中,就是 CommentEntity 实体的 commentable 方法:

$comment = web\Entity\CommentEntity::find(1);

$commentable = $comment->commentable;

CommentEntity 实体的 commentable 关联将返回 PostEntityVideoEntity 实例,其结果取决于评论所属的实体。

# 查询关联

由于 Entity 关联的所有类型都通过方法定义,你可以调用这些方法,而无需真实执行关联查询。另外,所有 Entity 关联类型用作 查询构造器,允许你在数据库上执行 SQL 之前,持续通过链式调用添加约束。

例如,假设一个博客系统的 UsersEntity 实体有许多关联的 PostEntity实体:


<?php

namespace web\Entity;

Cml\Entity\Entity;

class UsersEntity extends Entity
{
    /**
     * 获取该用户的所有文章。
     */
    public function posts()
    {
        return $this->hasMany('web\Entity\PostEntity');
    }
}

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

$user = web\Entity\UsersEntity::find(1);

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

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

# 关联方法 Vs. 动态属性

如果不需要向 Entity 关联查询添加额外的约束,可以像属性一样访问关联。例如,继续使用 UsersEntityPostEntity 示例实体,可以这样访问用户的全部文章:

$user = web\Entity\UsersEntity::find(1);

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

动态属性是「懒加载」的,这意味着它们仅在你真实访问关联数据时才被载入。因此,开发者经常使用 预加载 预先加载那些他们确知在载入实体后将访问的关联。对载入实体关联中必定被执行的 SQL 查询而言,预加载显著减少了查询的执行次数。

# 查询已存在的关联

在访问实体记录时,可能希望基于关联的存在限制查询结果。比如想要获取至少存在一条评论的所有文章,可以通过给 hasorHas 方法传递关联名称来实现:

// 获取至少存在一条评论的所有文章...
$posts = web\Entity\PostEntity::has('comments')->select();

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

// 获取评论超过三条的文章...
$posts = web\Entity\PostEntity::has('comments', '>=', 3)->select();

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

// 获取至少带有一条评论内容包含 foo% 关键词的文章...
$posts = web\Entity\PostEntity::whereHas('comments', function ($query) {
    $query->whereLike('content', false, 'foo', true);
})->select();

// 获取至少带有十条评论内容包含 foo% 关键词的文章...
$posts = web\Entity\PostEntity::whereHas('comments', function (Builder $query) {
    $query->whereLike('content', false, 'foo', true);
}, '>=', 10)->select();

# 查询不存在的关联

在访问实体记录时,可能希望基于关联不存在来限制查询结果。假设想要获取存在任何评论的文章,可以通过向 doesntHave 来实现:

$posts = web\Entity\PostEntity::whereDoesntHave('comments', function ( $query) {
    $query->whereLike('content', false, 'foo', true);
})->select();

# 关联实体计数

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

$posts = web\Entity\PostEntity::withCount('comments')->select();

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

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


$posts = web\Entity\PostEntity::withCount(['votes', 'comments' => function ( $query) {
    $query->where('content', 'foo');
}])->select();

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

# 预加载

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

<?php

namespace web\Entity;

Cml\Entity\Entity;

class BookEntity extends Entity
{
    /**
     * 获取书籍作者。
     */
    public function author()
    {
        return $this->belongsTo('web\Entity\AuthorEntity');
    }
}

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

$books = web\Entity\BookEntity::all();

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

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

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

$books = web\Entity\BookEntity::with('author')->select();

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

在这个例子中,仅执行了两个查询:

select * from books

select * from authors where id in (1, 2, 3, 4, 5, ...)

# 预加载多个关联

有时,你可能需要在单一操作中预加载几个不同的关联。要达成此目的,只要向 with 方法传递多个关联名称构成的数组参数:

 $books = web\Entity\BookEntity::with(['author', 'publisher'])->select();

# 嵌套预加载

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

$books = web\Entity\BookEntity::with('author.contacts')->select();

# 预加载指定列

并不是总需要获取关系的每一列。在这种情况下,Entity 允许你为关联指定想要获取的列:

$books = web\Entity\BookEntity::with(['author'=> 'id,name'])->select();

TIP

在使用这个特性时,一定要在要获取的列的列表中包含 id 列。

# 为预加载添加约束

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

$users = web\Entity\UsersEntity::with(['posts' => function ($query) {
        $query->where('title', 'first');
    }])->select();

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

$users = web\Entity\UsersEntity::with(['posts' => function ($query) {
    $query->orderBy('ctime', 'desc');
}])->select();

# 延迟预加载

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

$books = web\Entity\BookEntity::findMany();

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

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

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

# 插入 & 更新关联实体

# 保存方法

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

$comment = new web\Entity\CommentEntity(['message' => 'A new comment.']);

$post = web\Entity\Post::find(1);

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

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

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

$post = web\Entity\PostEntity::find(1);

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

# 新增方法

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

$post = web\Entity\PostEntity::find(1);

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

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

$post = web\Entity\PostEntity::find(1);

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

# 更新 belongsTo 关联

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

 $account = web\Entity\AccountEntity::find(10);
$user->account()->associate($account);
$user->save();

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

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

# 默认实体

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

//获取帖子的作者。
public function user()
{
    return $this->belongsTo('web\Entity\UsersEntity')->withDefault();
}

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

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

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

# 多对多关联

# 附加 / 分离

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

$user = web\Entity\UsersEntity::find(1);
$user->roles()->attach($roleId);

在将关系附加到实体时,还可以传递一组要插入到中间表中的附加数据:

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

当然,有时也需要移除用户的角色。可以使用 detach 移除多对多关联记录。detach 方法将会移除中间表对应的记录;但是这 2 个实体都将会保留在数据库中:

// 移除用户的一个角色...
$user->roles()->detach($roleId);

// 移除用户的所有角色...
$user->roles()->detach();

为了方便,attachdetach 也允许传递一个 ID 数组:

$user = web\Entity\UsersEntity::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]);