Laravel-关系预加载数量限制

Laravel-关系预加载数量限制

orzlee
2020-12-22 / 0 评论 / 753 阅读 / 正在检测是否收录...
温馨提示:
本文最后更新于2020年12月22日,已超过1388天没有更新,若内容或图片失效,请留言反馈。

Laravel.png

前言

最近开发一个项目,关于用户评论。评论可以被用户再次评论,当然只做了一级限制,没有做太多层级,可以回复某个评论中的特定用户(类似于@功能)

在输出评论列表中,是应该输出部分评论的回复,但是看似简单,实际情况却相对复杂。

分析

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...,其中有位开发者通过修改limittake方法来实现预加载关系数量限制,但是提交PR被laravel作者拒绝,于是封装成扩展eloquent-eager-limit

我安装后试了下,确实能达到效果,于是看了下源码。作者几乎把所有关系的limittake方法全部重写,然后封装成Trait,模型中使用该Trait相当于重写了limittake方法。使用方法和之前一样,但是要注意,一旦使用eloquent-eager-limitTrait后,该模型就不再有之前关系limittake的总限制效果了,相信laravel作者拒绝也是因为此原因。

来看看生成的语句:
sql.png

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
取消
扫码打赏
支付金额随意哦!

评论 (0)

取消