前言
最近开发一个项目,关于用户评论。评论可以被用户再次评论,当然只做了一级限制,没有做太多层级,可以回复某个评论中的特定用户(类似于@功能)。
在输出评论列表中,是应该输出部分评论的回复,但是看似简单,实际情况却相对复杂。
分析
在laravel中查询出两级并不难, 使用预加载可以轻松完成:
class Comment extends Model
{
...
public function comments()
{
return $this->hasMany($this, 'comment_id');
}
...
}
Comment::with('comments')->simplePaginate()
当需要限制评论回复的数量时,首先想到的是如下方法:
Comment::with(
[
'comments' => function (HasMany $query) {
$query->take(5);
},
]
)->simplePaginate()
但是返回的模型关系comments
被限制小于等5条,并不是每条评论的回复,而是整个查询结果的评论回复。如果第一条评论回复有10条,那么会显示5条,之后其他评论的回复全部为空。
其中的Sql语句是这样:
select * from `comments` where `comments`.`comment_id` in (1, 2, ...) limit 5
要实现每条评论显示N条回复并不难,只是回到了N + 1
的问题, 查询出列表后再加载关系:
$comments = Comment::simplePaginate();
$comments->each(function($comment) {
$comment->load(
[
'comments' => function (HasMany $query) {
$query->take(5);
},
]
);
});
Sql:
select * from `comments` where `comments`.`comment_id` in (1) limit 5
select * from `comments` where `comments`.`comment_id` in (2) limit 5
...
这样确实可以解决问题,但是非常笨。
在laravel-framework的issue中找到了类似问题Eager-loading with a limit on collection...,其中有位开发者通过修改limit
和take
方法来实现预加载关系数量限制,但是提交PR被laravel作者拒绝,于是封装成扩展eloquent-eager-limit。
我安装后试了下,确实能达到效果,于是看了下源码。作者几乎把所有关系的limit
和take
方法全部重写,然后封装成Trait,模型中使用该Trait相当于重写了limit
和take
方法。使用方法和之前一样,但是要注意,一旦使用eloquent-eager-limit的Trait后,该模型就不再有之前关系limit
或take
的总限制效果了,相信laravel作者拒绝也是因为此原因。
来看看生成的语句:
select laravel_table.*,
@laravel_row := if(@laravel_partition = `comment_id`, @laravel_row + 1, 1) as laravel_row,
@laravel_partition := `comment_id` from (select @laravel_row := 0, @laravel_partition := 0) as laravel_vars,
(select * from `comments` where `comments`.`comment_id` in (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16)
order by `comments`.`comment_id` asc) as laravel_table having laravel_row <= 10 order by laravel_row;
从Sql语句看来确实只用两次查询就可以获得结果,比之前load
遍历加载方法更优。
来看看最终效果:
{
"data": [
{
"id": 1,
"post_id": 2,
"comment_id": null,
"contents": "contents...",
"created_at": "2020-12-21 09:31:26",
"comments": [
{
"id": 24,
"post_id": 2,
"comment_id": 1,
"contents": "contents....",
"created_at": "2020-12-22 10:21:40",
...
},
{
"id": 18,
"post_id": 2,
"comment_id": 1,
"contents": "contents....",
"created_at": "2020-12-22 10:18:10",
...
},
...
],
},
{
"id": 7,
"post_id": 2,
"comment_id": null,
"contents": "contents....",
"created_at": "2020-12-22 09:38:07",
"comments": [
{
"id": 13,
"post_id": 2,
"comment_id": 7,
"contents": "contents....",
"created_at": "2020-12-22 10:11:02",
},
{
"id": 8,
"post_id": 2,
"comment_id": 7,
"contents": "contents....",
"created_at": "2020-12-22 09:59:43",
}
...
]
},
...
],
...
"status": "success",
"code": 200
结语
其实也可以手动写Sql语句,缩成一条语句查询。但是太过于复杂的Sql并不易于维护,而且性能并不是一次复杂查询就一定比两次查询快。损失一点性能提高代码可维护性还是赚的。如果项目用户量较大,就要考虑缓存等其他技术优化了。
评论 (0)