3 条 sql 是实现知乎评论,7 条 sql 实现点赞 + 评论,且可扩展

数据结构

文章数据结构

Post
    - id
    - title
    - body
    - vote_count
    - comment_count

评论数据结构

Comment
    - id
    - title
    - body
    - commentable_id
    - commentable_type
    - parent_id
    - user_id
    - first_depth_id 一级评论id
    - vote_count
    - comment_count

点赞数据结构

    Vote
        - id
        - user_id
        - voteable_id
        - voteable_type

Model

Post

public function comments()
    {
        return $this->morphMany(Comment::class, 'commentable');
    }

    public function votes()
    {
        return $this->morphMany(Vote::class, 'voteable');
    }

Comment

 public function commentable()
    {
        return $this->morphTo();
    }

    public function parent()
    {
        return $this->belongsTo(Comment::class,'parent_id');
    }

    public function children()
    {
        return $this->hasMany(Comment::class, 'parent_id');
    }

    public function user()
    {
        return $this->belongsTo(User::class);
    }

    public function votes()
    {
        return $this->morphMany('App\Vote', 'voteable');
    }

    public function seconds()
    {
        return $this->hasMany(Comment::class,'first_depth_id');
    }

Vote

    public function voteable()
    {
        return $this->morphTo();
    }

    public function user()
    {
        return $this->belongsTo(User::class);
    }

查询评论

1直接用模型查询评论及用户的数据,数据类型也符合前端的要求

直接用sql去查文章的评论及用户(不建议),因为用了7条sql

 $posts = Post::with(['comments'=>function ($query){
        $query->where('parent_id',null)->with(['user','seconds'=>function($query){
            $query->with('user','parent.user');
        }]);
    }])->get();

debug 显示
file
返回到前端的数据
file

2查询出所有评论及用户,再去解析数据,返回前端。

该查询用到三条sql(建议)

$posts = Post::with('comments','comments.user')->get();
$posts = collect($posts->toArray())->map(function ($post){
    $nestedKeys = [];
    $post['comments'] = array_column($post['comments'], null, 'id');
    foreach ($post['comments'] as $key=>$comment){
        if(!$parent_id=$comment['parent_id']){
            continue;
        }
        if (array_key_exists($first_depth_id=$comment['first_depth_id'], $post['comments'])) {
            if(!isset($post['comments'][$first_depth_id]['seconds'])){
                $post['comments'][$first_depth_id]['seconds'] = [];
            }
            $comment['parent']=$post['comments'][$parent_id];
            $post['comments'][$first_depth_id]['seconds'][] = $comment;
            $nestedKeys[]=$key;
        }else{
            $nestedKeys[]=$key;
        }
    }
    foreach ($nestedKeys as $val){
        unset($post['comments'][$val]);
    }
    return $post;
});

debug
file
返回到前端的数据,可以看到和上方的返回数据是一样的,而这个只用了三条sql
file

查询点赞+评论

由于点赞没有无限极的影响,所以实现起来比较简单

直接用模型实现

在评论的基础上加上点赞和点赞的用户即可(不建议,因为sql增加到12条)

$posts = Post::with(['comments'=>function ($query){
            $query->where('parent_id',null)->with(['votes'=>function($query){
                $query->with('user');
            },'user','seconds'=>function($query){
                $query->with(['user','parent.user','votes'=>function($query){
                    $query->with('user');
                }]);
            }]);
        },'votes'=>function($query){
            $query->with('user');
        }])->get();

debug
file
返回的数据
file

先查出总数据再去解析

在评论的基础上加个点赞即可,用了7条sql(建议)

$posts = Post::with('comments','comments.user','votes.user','comments.votes.user')->get();
$posts = collect($posts->toArray())->map(function ($post){
    $nestedKeys = [];
    $post['comments'] = array_column($post['comments'], null, 'id');
    foreach ($post['comments'] as $key=>$comment){
        if(!$parent_id=$comment['parent_id']){
            continue;
        }
        if (array_key_exists($first_depth_id=$comment['first_depth_id'], $post['comments'])) {
            if(!isset($post['comments'][$first_depth_id]['seconds'])){
                $post['comments'][$first_depth_id]['seconds'] = [];
            }
            $comment['parent']=$post['comments'][$parent_id];
            $post['comments'][$first_depth_id]['seconds'][] = $comment;
            $nestedKeys[]=$key;
        }else{
            $nestedKeys[]=$key;
        }
    }
    foreach ($nestedKeys as $val){
        unset($post['comments'][$val]);
    }
    return $post;
});

debug 增加的4条sql是不可避免的
file
返回的数据
file

总结

一般来说查询sql的时间,要多于程序解析的时间,所以把需要用到的数据,查询出来,再去由程序去解析成相应的数据类型,而不要用sql去实现。
【2019/8/8 更新】
上面的总结过于粗暴~ 数据量大的话,要用到其他优化方式,比如加缓存。具体业务,在具体分析,这里抛砖引玉

本作品采用《CC 协议》,转载必须注明作者和本文链接
Make everything simple instead of making difficulties as simple as possible
本帖由系统于 6年前 自动加精
《L02 从零构建论坛系统》
以构建论坛项目 LaraBBS 为线索,展开对 Laravel 框架的全面学习。应用程序架构思路贴近 Laravel 框架的设计哲学。
《L01 基础入门》
我们将带你从零开发一个项目并部署到线上,本课程教授 Web 开发中专业、实用的技能,如 Git 工作流、Laravel Mix 前端工作流等。
讨论数量: 17
foreach ($post['comments'] as $comment){
    $array[$comment['id']] = $comment;
}
$post['comments'] = $array;

可以使用$post['comments'] = array_column($post['comments'], null, 'id')代替,PHP原生库函数是使用C实现的,效率想必会更高

6年前 评论
foreach ($post['comments'] as $comment){
    $array[$comment['id']] = $comment;
}
$post['comments'] = $array;

可以使用$post['comments'] = array_column($post['comments'], null, 'id')代替,PHP原生库函数是使用C实现的,效率想必会更高

6年前 评论
jcc123

@xinhuo 已优化,感谢

6年前 评论
hareluya

性能调优范畴不只是语句,还要综合考虑数据库和主机性能。有的时候数据库承载运算也许会更好。
要么空间换时间,要么时间换时间。

6年前 评论

帖子写得挺好的,学习了,谢谢!

6年前 评论

这个点赞信息为什么要查出来?不是应该给每条评论加一个是否点赞的标识吗? $nestedKeys[]=$key;这句每次都执行,没必要写else了,当然不影响也。

6年前 评论
jcc123

@jiacongcong 自身理解,写这篇的目的是要把所有数据,都查询出来,然后再根据具体的需要修改成自己想要的部分。判断是否点赞,自己认为有两个方法。
一是用空间换取时间,在评论字段中,增加一个冗余字段,记录该评论的所有点赞id

Comment
    - id
    - title
    - body
    - commentable_id
    - commentable_type
    - parent_id
    - user_id
    - first_depth_id 一级评论id
    - vote_count
    - comment_count
    - vote_ids  (1-2-3-4)这样的形式

然后

$posts = Post::with('comments','comments.user','votes.user')->get();
$posts = collect($posts->toArray())->map(function ($post){
    $nestedKeys = [];
    $post['comments'] = array_column($post['comments'], null, 'id');
    foreach ($post['comments'] as $key=>$comment){
        if(in_array($user_id=1, explode('-',$comment['vote_ids'])){//也可以放在Model里去实现
            $comment['if_vote']=true;
        }else{
            $comment['if_vote']=false;
        }
        if(!$parent_id=$comment['parent_id']){
            continue;
        }
        if (array_key_exists($first_depth_id=$comment['first_depth_id'], $post['comments'])) {
            if(!isset($post['comments'][$first_depth_id]['seconds'])){
                $post['comments'][$first_depth_id]['seconds'] = [];
            }
            $comment['parent']=$post['comments'][$parent_id];
            $post['comments'][$first_depth_id]['seconds'][] = $comment;
            $nestedKeys[]=$key;
        }else{
            $nestedKeys[]=$key;
        }
    }
    foreach ($nestedKeys as $val){
        unset($post['comments'][$val]);
    }
    return $post;
});

二用时间换取空间。

$posts = Post::with('comments','comments.user','votes.user','comments.votes')->get();
$posts = collect($posts->toArray())->map(function ($post){
    $nestedKeys = [];
    $post['comments'] = array_column($post['comments'], null, 'id');
    foreach ($post['comments'] as $key=>$comment){
        if(array_search($user_id=1, array_column($comment['votes'], 'user_id')) !== False){
            $comment['if_vote']=true;
        }else{
            $comment['if_vote']=false;
        }
        if(!$parent_id=$comment['parent_id']){
            continue;
        }
        if (array_key_exists($first_depth_id=$comment['first_depth_id'], $post['comments'])) {
            if(!isset($post['comments'][$first_depth_id]['seconds'])){
                $post['comments'][$first_depth_id]['seconds'] = [];
            }
            $comment['parent']=$post['comments'][$parent_id];
            $post['comments'][$first_depth_id]['seconds'][] = $comment;
            $nestedKeys[]=$key;
        }else{
            $nestedKeys[]=$key;
        }
    }
    foreach ($nestedKeys as $val){
        unset($post['comments'][$val]);
    }
    return $post;
});
6年前 评论

@jcc123 第二种好些吧,第一种加字段感觉不是很好。嘻嘻

6年前 评论

我是查详情用了这种方法,就一个文章,这种评论分页岂不是,每次得获取所有评论,自己再计算。

6年前 评论
yourself

foreach 嵌套 if的代码 实在是。。。。让我想起了 tp3.2

6年前 评论

@yourself 大神有好的方法可以分享一下。

6年前 评论
yourself

@jiacongcong 不是什么大神,好方法就是最小化分解,判断和处理拆分,在调用位置引用处理。单一职责,一个方法只处理一件事,易扩展,可插拔形式。

6年前 评论

laravel萌新,请问这个debug工具叫啥,能看到实际执行的sql数目

6年前 评论

@windpuller clockwork 你google一下吧

6年前 评论

看到这么多foreach 就头痛 不是好代码。。。

6年前 评论
$posts = Post::with('comments','comments.user')->get();
$posts = collect($posts->toArray())->map(function ($post){
.
.
.
});

如果是我写的话,这里应该不会将 $posts 转换成数组后又转成集合。:)

6年前 评论
jcc123

@superSnail 不转的话,可能会使用到Model里的方法

6年前 评论
哇酷哦 4年前
DonnyLiu

大佬牛逼

4年前 评论

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!