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年前 自动加精
《L05 电商实战》
从零开发一个电商项目,功能包括电商后台、商品 & SKU 管理、购物车、订单管理、支付宝支付、微信支付、订单退款流程、优惠券等
《L04 微信小程序从零到发布》
从小程序个人账户申请开始,带你一步步进行开发一个微信小程序,直到提交微信控制台上线发布。
讨论数量: 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年前 评论

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