关联

October CMS Documentation Docs

关联

数据库表通常相互关联。 例如,一篇博客文章可能有很多评论,或者某个订单可能与发布该订单的用户相关。 October 使管理和处理这些关联变得容易,并支持多种不同类型的关联。

# 定义关联

模型关联被定义为模型类的属性。 定义关联的示例:

class User extends Model
{
    public $hasMany = [
        'posts' => \Acme\Blog\Models\Post::class
    ]
}

像模型本身这样的关联也可以作为强大的 查询构建器,将关联作为函数访问提供了强大的方法链接和查询功能。 例如:

$user->posts()->where('is_active', true)->get();

也可以将关联作为属性访问:

$user->posts;

# 详细定义

每个定义都可以是一个数组,其中键是关联名称,值是细节数组。 详细信息数组的第一个值始终是关联模型类名称,所有其他值都是必须具有键名的参数。

public $hasMany = [
    'posts' => [\Acme\Blog\Models\Post::class, 'delete' => true]
];

以下是可用于所有关联的参数:

参数 描述
order 多条记录的排序顺序。
conditions 使用原始 where 查询语句过滤关联。
scope 使用提供的范围方法过滤关联。
push 如果设置为 false,则不会通过 push 保存此关联,默认值:true
delete 如果设置为true,如果主模型被删除或关联被破坏,关联模型将被删除,默认:false
replicate 如果设置为 true,相关模型将通过 replicate 方法复制或关联。 默认值:false
relationClass 为相关对象指定自定义类名。

使用 orderconditions 参数的过滤器示例。

public $belongsToMany = [
    'categories' => [
        \Acme\Blog\Models\Category::class,
        'order'      => 'name desc',
        'conditions' => 'is_active = 1'
    ]
];

使用 scope 参数的示例过滤器。

class Post extends Model
{
    public $belongsToMany = [
        'categories' => [
            \Acme\Blog\Models\Category::class,
            'scope' => 'isActive'
        ]
    ];
}

class Category extends Model
{
    public function scopeIsActive($query)
    {
        return $query->where('is_active', true)->orderBy('name', 'desc');
    }
}

# 关联类型

可以使用以下关联类型:

# 一对一

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

<?php namespace Acme\Blog\Models;

use Model;

class User extends Model
{
    public $hasOne = [
        'phone' => \Acme\Blog\Models\Phone::class
    ];
}

hasOne 方法的第一个参数是关联模型的类名。一旦定义了模型关联,我们就可以使用 同名的模型属性检索相关记录。动态属性允许你像访问模型中定义的属性一样访问关联方法:

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

模型会基于模型名决定外键名称。 在这种情况下,会自动假设 Phone 模型有一个 user_id 外键。 如果你想覆盖这个约定,您可以将 key 参数传递定义:

public $hasOne = [
    'phone' => [\Acme\Blog\Models\Phone::class, 'key' => 'my_user_id']
];

此外,该模型假设外键的值应与父级的id字段匹配。 换句话说,它会在Phone记录的user_id列中查找用户表的id字段的值。 如果您希望关联使用 id 以外的定义键名,可以将 otherKey 参数传递定义:

public $hasOne = [
    'phone' => [\Acme\Blog\Models\Phone::class, 'key' => 'my_user_id', 'otherKey' => 'my_id']
];

# 定义反向关联

我们已经能从 User 模型访问到 Phone 模型了。现在,让我们再在 Phone 模型上定义一个关联,这个关联能让我们访问到拥有该电话的 User 模型。我们可以使用与 hasOne 方法对应的 belongsTo 方法来定义反向关联:

class Phone extends Model
{
    public $belongsTo = [
        'user' => \Acme\Blog\Models\User::class
    ];
}

在上面的例子中, 模型会尝试匹配 Phone 模型上的 user_idUser 模型上的 id 。它是通过检查关联方法的名称并使用 _id 作为后缀名来确定默认外键名称的。但是,如果 Phone 模型的外键不是 user_id,那么可以将自定义键名作为key参数传递给 belongsTo 方法:

public $belongsTo = [
    'user' => [Acme\Blog\Models\User::class, 'key' => 'my_user_id']
];

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

public $belongsTo = [
    'user' => [\Acme\Blog\Models\User::class, 'key' => 'my_user_id', 'otherKey' => 'my_id']
];

# 默认模型

belongsTo关联允许你指定默认模型,当给定关联为 null 时,将会返回默认模型。 这种模式被称作 Null 对象模式 (opens new window) ,可以减少你代码中不必要的检查。在下面的例子中,如果发布的帖子没有找到作者, user 关联会返回一个空的 Acme\Blog\Models\User 模型:

public $belongsTo = [
    'user' => [\Acme\Blog\Models\User::class, 'default' => true]
];

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

public $belongsTo = [
    'user' => [
        \Acme\Blog\Models\User::class,
        'default' => ['name' => 'Guest']
    ]
];

# 一对多

『一对多』关联用于定义单个模型拥有任意数量的其它关联模型。例如,一篇博客文章可能会有无限多条评论。正如其它所有的模型关联一样,一对多关联的定义也是在 模型中写一个$hasMany方法:

class Post extends Model
{
    public $hasMany = [
        'comments' => \Acme\Blog\Models\Comment::class
    ];
}

记住一点,模型将会自动确定 Comment 模型的外键属性。按照约定,模型将会使用所属模型名称的 『snake case』形式,再加上 _id 后缀作为外键字段。因此,在上面这个例子中,模型将假定 Comment 对应到 Post 模型上的外键就是 post_id

一旦关联被定义好以后,就可以通过访问 Post 模型的 comments 属性来获取评论的集合。记住,由于模型 提供了『动态属性』 ,所以我们可以像访问模型的属性一样访问关联方法:

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

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

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

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

正如 hasOne 方法一样,你也传递 keyotherKey参数来覆盖外键与本地键:

public $hasMany = [
    'comments' => [\Acme\Blog\Models\Comment::class, 'key' => 'my_post_id', 'otherKey' => 'my_id']
];

# 一对多 (反向)

现在,我们已经能获得一篇文章的所有评论,接着再定义一个通过评论获得所属文章的关联关系。这个关联是 hasMany 关联的反向关联,需要在子级模型中使用 belongsTo 方法定义它:

class Comment extends Model
{
    public $belongsTo = [
        'post' => \Acme\Blog\Models\Post::class
    ];
}

这个关联定义好以后,我们就可以通过访问 Comment 模型的 post 这个『动态属性』来获取关联的 Post 模型了:

$comment = Comment::find(1);

echo $comment->post->title;

在上面的例子中,模型会尝试用 Comment 模型的 post_idPost 模型的 id 进行匹配。默认外键名是 模型依据关联名,并在关联名后加上 _id再加上主键字段名作为后缀确定的。当然,如果 Comment 模型的外键不是 post_id,那么可以将自定义键名作为key参数传递给 belongsTo 方法:

public $belongsTo = [
    'post' => [\Acme\Blog\Models\Post::class, 'key' => 'my_post_id']
];

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

public $belongsTo = [
    'post' => [Acme\Blog\Models\Post::class, 'key' => 'my_post_id', 'otherKey' => 'my_id']
];

# 多对多

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

下面是一个显示用于创建连接表的数据库表结构的示例。

Schema::create('role_user', function($table)
{
    $table->integer('user_id')->unsigned();
    $table->integer('role_id')->unsigned();
    $table->primary(['user_id', 'role_id']);
});

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

class User extends Model
{
    public $belongsToMany = [
        'roles' => \Acme\Blog\Models\Role::class
    ];
}

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

$user = User::find(1);

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

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

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

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

public $belongsToMany = [
    'roles' => [\Acme\Blog\Models\Role::class, 'table' => 'acme_blog_role_user']
];

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

public $belongsToMany = [
    'roles' => [
        \Acme\Blog\Models\Role::class,
        'table'    => 'acme_blog_role_user',
        'key'      => 'my_user_id',
        'otherKey' => 'my_role_id'
    ]
];

# 定义反向关联

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

class Role extends Model
{
    public $belongsToMany = [
        'users' => \Acme\Blog\Models\User::class
    ];
}

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

# 获取中间表字段

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

$user = User::find(1);

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

需要注意的是,我们获取的每个 Role 模型对象,都会被自动分配一个 pivot 属性,它代表中间表的一个模型对象,并且可以像其他的模型一样使用。

默认情况下,pivot 对象只包含两个关联模型的主键,如果你的中间表里还有其他额外字段,你必须在定义关联时明确指出:

public $belongsToMany = [
    'roles' => [
        \Acme\Blog\Models\Role::class,
        'pivot' => ['column1', 'column2']
    ]
];

如果你想让中间表自动维护 created_atupdated_at 时间戳,那么在定义关联时附加上 timestamps 方法即可

public $belongsToMany = [
    'roles' => [
        \Acme\Blog\Models\Role::class,
        'timestamps' => true
    ]
];

如果你想定义一个自定义模型来表示你关联的中间表,你可以在定义关联时使用 pivotModel 属性。 自定义多对多中间表模型应扩展October\Rain\Database\Pivot类,而自定义多态多对多中间表模型应扩展October\Rain\Database\MorphPivot类。

public $belongsToMany = [
    'roles' => [
        \Acme\Blog\Models\Role::class,
        'pivotModel' => \Acme\Blog\Models\UserRolePivot::class
    ]
];

这些是 belongsToMany 关联支持的参数:

参数 描述
table 连接表的名称。
key 定义模型键的字段名称(在数据透视表内)。 默认值由模型名称和_id后缀组合而成,即user_id
parentKey 定义模型键的字段名称(在定义模型表中)。 默认值:id
otherKey 关联模型键的字段名称(在数据透视表内)。 默认值由模型名称和_id后缀组合而成,即role_id
relatedKey 关联模型键的字段名称(在关联模型表内)。 默认值:id
pivot 在中间表中找到的关联字段数组,属性可通过 $model->pivot 获得。
pivotModel 指定访问中间关联时要返回的自定义模型类。 默认为 October\Rain\Database\Pivot 而对于多态关联为 October\Rain\Database\MorphPivot
timestamps 如果为true,则关联表应包含created_atupdated_at字段。 默认值:false

# 远程一对多关联

远程"一对多"关联提供了方便、简短的方式通过中间的关联来获得远层的关联。例如,一个 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 访问到一个国家下所有的用户文章。为了完成这个查询,模型会先检查中间表 userscountry_id 字段,找到所有匹配的用户 ID 后,使用这些 ID,在 posts 表中完成查找。

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

class Country extends Model
{
    public $hasManyThrough = [
        'posts' => [
            \Acme\Blog\Models\Post::class,
            'through' => \Acme\Blog\Models\User::class
        ],
    ];
}

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

当执行关联查询时,通常会使用模型约定的外键名。如果你想要自定义关联的键,可以通过给 hasManyThrough 方法传递key, otherKeythroughKey参数实现,key参数表示中间模型的外键名,throughKey参数表示最终模型的外键名。otherKey参数表示本地键名:

public $hasManyThrough = [
    'posts' => [
        \Acme\Blog\Models\Post::class,
        'key' => 'my_country_id',
        'through' => \Acme\Blog\Models\User::class,
        'throughKey' => 'my_user_id',
        'otherKey' => 'my_id'
    ],
];

# 远程一对一

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

users
    id - integer
    supplier_id - integer

suppliers
    id - integer

history
    id - integer
    user_id - integer

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

class Supplier extends Model
{
    public $hasOneThrough = [
        'userHistory' => [
            \Acme\Supplies\Model\History::class,
            'through' => \Acme\Supplies\Model\User::class
        ],
    ];
}

传递给 hasOneThrough 方法的第一个参数是希望访问的模型名称,through参数是中间模型的名称。

当执行关联查询时,通常会使用模型约定的外键名。如果你想要自定义关联的键,可以通过给 hasManyThrough 方法传递key, otherKeythroughKey参数实现,key参数表示中间模型的外键名,throughKey参数表示最终模型的外键名。otherKey参数表示本地键名:

public $hasOneThrough = [
    'userHistory' => [
        \Acme\Supplies\Model\History::class,
        'key' => 'supplier_id',
        'through' => \Acme\Supplies\Model\User::class,
        'throughKey' => 'user_id',
        'otherKey' => 'id'
    ],
];

# 多态关联

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

# 多态一对一

# 表结构

一对一多态关联与简单的一对一关联类似;不过,目标模型能够在一个关联上从属于多个模型。例如,您的 员工产品 可能共享一个关联到 照片 模型的关系。使用一对一多态关联允许使用一个唯一图片列表同时用于员工列表和产品列表。让我们先看看表结构:

staff
    id - integer
    name - string

products
    id - integer
    price - integer

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

要特别留意 photos 表的 imageable_idimageable_type 列。 imageable_id 列包含产品或员工的 ID 值,而 imageable_type 列包含的则是父模型的类名。ORM在访问 imageable 时使用 imageable_type 列来判断父模型的 "类型"

# 模型结构

接下来,再看看建立关联的模型定义:

class Photo extends Model
{
    public $morphTo = [
        'imageable' => []
    ];
}

class Staff extends Model
{
    public $morphOne = [
        'photo' => [\Acme\Blog\Models\Photo::class, 'name' => 'imageable']
    ];
}

class Product extends Model
{
    public $morphOne = [
        'photo' => [\Acme\Blog\Models\Photo::class, 'name' => 'imageable']
    ];
}

# 获取关联

一旦定义了表和模型,就可以通过模型访问此关联。比如,要访问员工的照片,可以使用 photo 动态属性:

$staff = Staff::find(1);

$photo = $staff->photo

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

$photo = Photo::find(1);

$imageable = $photo->imageable;

Photo 模型的 imageable 关联将返回 StaffProduct 实例,其结果取决于图片属性哪个模型。

# 多态一对多

# 表结构

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

# 模型结构

接下来,看看构建这种关联的模型定义:

class Comment extends Model
{
    public $morphTo = [
        'commentable' => []
    ];
}

class Post extends Model
{
    public $morphMany = [
        'comments' => [\Acme\Blog\Models\Comment::class, 'name' => 'commentable']
    ];
}

class Product extends Model
{
    public $morphMany = [
        'comments' => [\Acme\Blog\Models\Comment::class, 'name' => 'commentable']
    ];
}

# 获取关联

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

$post = Author\Plugin\Models\Post::find(1);

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

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

$comment = Author\Plugin\Models\Comment::find(1);

$commentable = $comment->commentable;

Comment 模型的 commentable 关联将返回 PostVideo 实例,其结果取决于评论所属的模型。

您还可以通过使用 morphTo 关联的名称设置属性来更新关联模型的所有者,在本例中为 commentable

$comment = Author\Plugin\Models\Comment::find(1);
$video = Author\Plugin\Models\Video::find(1);

$comment->commentable = $video;
$comment->save()

# 多态多对多

# 表结构

多对多多态关联比 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 模型都有调用基础模型类上 morphToMany 方法的 tags 方法:

class Post extends Model
{
    public $morphToMany = [
        'tags' => [\Acme\Blog\Models\Tag::class, 'name' => 'taggable']
    ];
}

# 定义反向关联关系

下面,需要在 Tag 模型上为每个关联模型定义一个方法。在这个示例中,我们将会定义 posts 方法和 videos 方法:

class Tag extends Model
{
    public $morphedByMany = [
        'posts'  => [\Acme\Blog\Models\Post::class, 'name' => 'taggable'],
        'videos' => [\Acme\Blog\Models\Video::class, 'name' => 'taggable']
    ];
}

# 获取关联

一旦定义了数据库表和模型,就可以通过模型访问关联。例如,可以使用 tags 动态属性访问文章的所有标签:

$post = Post::find(1);

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

还可以访问执行 morphedByMany 方法调用的方法名来从多态模型获取其所属模型。在这个示例中,就是 Tag 模型的 postsvideos 方法。可以像动态属性一样访问这些方法:

$tag = Tag::find(1);

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

# 自定义多态类型

默认情况下,使用完全限定类名存储关联模型类型。在上面的一对多示例中, 因为 Photo 可能从属于一个 Staff 或一个 Product,默认的 commentable_type 就将分别是 Acme\Blog\Models\StaffAcme\Blog\Models\Product

使用自定义多态类型可以让您将数据库与应用程序的内部结构分离。 你可以定义一个关联"变形映射"来为每个模型提供一个自定义名称,而不是类名:

use October\Rain\Database\Relations\Relation;

Relation::morphMap([
    'staff' => \Acme\Blog\Models\Staff::class,
    'product' => \Acme\Blog\Models\Product::class,
]);

插件注册文件boot 方法中注册 morphMap 的最常见位置。

# 查询关联

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

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

class User extends Model
{
    public $hasMany = [
        'posts' => \Acme\Blog\Models\Post::class
    ];
}

# 通过关联方法访问

您可以使用 posts 方法查询 posts 关联并为关联添加额外的约束。 这使您能够在关联上扩展任何 查询构造器 方法。

$user = User::find(1);

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

$post = $user->posts()->first();

# 通过动态属性访问

如果您不需要向关联查询添加额外的约束,您可以简单地访问该关联,就像它是一个属性一样。 例如,继续使用我们的 UserPost 示例模型,我们可以使用 $user->posts 属性来访问用户的所有帖子。

$user = User::find(1);

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

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

# 查询已存在的关联

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

// 检索所有至少有一条评论的帖子...
$posts = Post::has('comments')->get();

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

// Retrieve all posts that have three or more comments...
$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();

# 关联记录计数

在某些情况下,您可能想要计算在给定关联定义中找到的相关记录的数量。 withCount 方法可用于在所选模型中包含 {relation}_count 列。

$users = User::withCount('roles')->get();

foreach ($users as $user) {
    echo $user->roles_count;
}

withCount 方法支持多个关联以及额外的查询约束。

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

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

您可以使用 loadCount 方法懒加载计数字段。

$user = User::first();
$user->loadCount('roles');

还支持其他查询约束。

$user->loadCount(['roles' => function ($query) {
    $query->where('clearance', '>', 5);
}])

# 预加载

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

class Book extends Model
{
    public $belongsTo = [
        'author' => \Acme\Blog\Models\Author::class
    ];
}

现在让我们检索所有书籍及其作者:

$books = Book::all();

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

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

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

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

# 嵌套预加载

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

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

# 为预加载添加约束

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

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

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

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

# 延迟预加载

有时您可能需要在已检索到父模型后立即加载关联。 例如,如果您需要动态决定是否加载关联模型,这可能很有用:

$books = Book::all();

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

如果您需要在预加载查询上设置额外的查询约束,您可以将 Closure 传递给 load 方法:

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

# 插入关联模型

就像查询关系 一样,October支持使用方法或动态属性方法定义关联。 例如,也许您需要为 Post 模型插入一个新的 Comment。 您可以直接从关联中插入 Comment,而不是手动设置 Comment 上的 post_id 属性。

# 通过关联方法插入

October为新模型添加关联提供了便捷的方法。 主要可以将模型添加到关联中或从关联中删除。 在每种情况下,分别关联或解除关联。

# 添加方法

使用 add 方法关联一个新的关联。

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

$post = Post::find(1);

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

请注意,我们没有将 comments 关联作为动态属性访问。 相反,我们调用 comments 方法来获取关联的实例。 add 方法会自动将适当的 post_id 值添加到新的 Comment 模型中。

如果需要保存多个相关模型,可以使用 addMany 方法:

$post = Post::find(1);

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

# 删除方法

相比之下,remove 方法可用于解除关联关系,使其成为无效记录。

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

在多对多关联的情况下,记录将从关联的集合中删除。

$post->categories()->remove($category);

在"belongsTo"关联的情况下,您可以使用 dissociate 方法,该方法不需要将相关模型传递给它。

$post->author()->dissociate();

# 使用透视数据添加

当处理多对多关系时,add 方法接受一个额外的中间"透视"表属性数组作为其第二个参数作为数组。

$user = User::find(1);

$pivotData = ['expires' => $expires];

$user->roles()->add($role, $pivotData);

add 方法的第二个参数还可以指定 延迟绑定 在作为字符串传递时使用的会话密钥。 在这些情况下,可以将透视数据作为第三个参数提供。

$user->roles()->add($role, $sessionKey, $pivotData);

# 创建方法

虽然 addaddMany 接受完整的模型实例,但您也可以使用 create 方法,该方法接受 PHP 属性数组,创建模型并将其插入数据库。

$post = Post::find(1);

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

在使用 create 方法之前,请务必查看有关属性 批量赋值 的文档,因为 PHP 数组中的属性受模型的"可填充"定义的限制。

# 通过动态属性插入

可以通过它们的属性直接设置关联,就像您访问它们一样。 使用这种方法设置关联将覆盖以前存在的任何关联。 之后应该像使用任何属性一样保存模型。

$post->author = $author;

$post->comments = [$comment1, $comment2];

$post->save();

或者,您可以使用主键设置关联,这在处理 HTML 表单时很有用。

// 分配给 ID 为 3 的作者
$post->author = 3;

// 分配 ID 为 1、2 和 3 的评论
$post->comments = [1, 2, 3];

$post->save();

可以通过将 NULL 值分配给属性来解除关联。

$post->author = null;

$post->comments = null;

$post->save();

延迟绑定 类似,在不存在的模型上定义的关联在内存中被延迟,直到它们被保存。 在此示例中,帖子尚不存在,因此无法通过 $post->comments 在评论上设置 post_id 属性。 因此,关联会延迟到通过调用save方法创建帖子为止。

$comment = Comment::find(1);

$post = new Post;

$post->comments = [$comment];

$post->save();

# 多对多关联

# 附加 / 分离

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

$user = User::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 = 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]);

# 更新父级时间戳

当一个模型属 belongsTo 或者 belongsToMany 另一个模型时, 例如 Comment 属于 Post,有时更新子模型导致更新父模型时间戳非常有用。例如,当 Comment 模型被更新时,您要自动"触发"父级 Post 模型的 updated_at 时间戳的更新。只要在子模型加一个包含关联名称的 touches 属性即可:

class Comment extends Model
{
    /**
     * 要触发的所有关联关系
     */
    protected $touches = ['post'];

    /**
     * Relations
     */
    public $belongsTo = [
        'post' => \Acme\Blog\Models\Post::class
    ];
}

现在,当你更新一个 Comment 时,对应父级 Post 模型的 updated_at 字段同时也会被更新:

$comment = Comment::find(1);

$comment->text = '编辑此评论!';

$comment->save();

# 延迟绑定

延迟绑定允许您延迟绑定模型关联,直到主记录提交更改。如果您需要准备一些模型(例如文件上传)并将它们关联到另一个尚不存在的模型,这将特别有用。

您可以使用 session key 将任意数量的 slave 模型延迟保定到 master 模型。当主记录与会话键一起保存时,与从属记录的关联会自动为您更新。后端 表单行为 自动支持延迟绑定,但您可能希望在其他地方使用此功能。

# 生成会话密钥

延迟绑定需要会话密钥。您可以将会话密钥视为事务标识符。相同的会话密钥应该用于绑定/解除绑定关联和保存主模型。您可以使用 PHP uniqid() 函数生成会话密钥。请注意,表单助手 会自动生成一个包含会话密钥的隐藏字段。

$sessionKey = uniqid('session_key', true);

# 延迟关联绑定

除非帖子被保存,否则下一个示例中的评论不会添加到帖子中。

$comment = new Comment;
$comment->content = "Hello world!";
$comment->save();

$post = new Post;
$post->comments()->add($comment, $sessionKey);

注意$post 对象尚未保存,但如果保存,将创建关联。

# 推迟解除绑定的关系

除非保存帖子,否则不会删除下一个示例中的评论。

$comment = Comment::find(1);
$post = Post::find(1);
$post->comments()->remove($comment, $sessionKey);

# 列出所有绑定

使用关联的 withDeferred 方法加载所有记录,包括延迟的。 结果也将包括现有关系。

$post->comments()->withDeferred($sessionKey)->get();

# 取消所有绑定

取消延迟绑定并删除从属对象而不是让它们成为无效对象是个好主意.

$post->cancelDeferred($sessionKey);

# 提交所有绑定

您可以在保存主模型时提交(绑定或取消绑定)所有延迟绑定,方法是为save方法的第二个参数提供会话密钥。

$post = new Post;
$post->title = "First blog post";
$post->save(null, $sessionKey);

同样的方法适用于模型的 create 方法:

$post = Post::create(['title' => 'First blog post'], $sessionKey);

# 延迟提交绑定

如果您在保存时无法提供 $sessionKey,您可以随时使用以下代码提交绑定:

$post->commitDeferred($sessionKey);

# 清理无效的绑定

销毁所有尚未提交且超过 1 天的绑定:

October\Rain\Database\Models\DeferredBinding::cleanUp(1);

注意:October CMS 会自动销毁超过 5 天的延迟绑定。 当后端用户登录系统时会触发。

# 禁用延迟绑定

有时您可能需要完全禁用给定模型的延迟绑定,例如,如果您从单独的数据库连接加载它。 您需要确保模型的 sessionKey 属性在内部保存方法中的 pre 和 post 延迟绑定钩子运行之前为 null。 为此,您可以绑定到模型的 model.saveInternal 事件。

public function __construct()
{
    $result = parent::__construct(...func_get_args());

    $this->bindEvent('model.saveInternal', function () {
        $this->sessionKey = null;
    });

    return $result;
}

注意:这将为您覆盖此应用的任何模型完全禁用延迟绑定。