【问题标题】:Laravel - How to avoid N+1 problem when loading more results in blade?Laravel - 在刀片中加载更多结果时如何避免 N+1 问题?
【发布时间】:2020-01-06 12:36:45
【问题描述】:

我正在建立一个有提交的网站,这些提交有 cmets。这些 cmets 的结构很像 reddit 的结构。这是一张供参考的图片:

这些 cmets 有很多关系(auth 用户是否保存/upvoted/downvoted 评论,评论是否有孩子,谁创建了评论)。

所以为了避免一堆不必要的服务器查询,我渴望加载这些关系。

$comments = Comment::with(['children','owner','savedComments','votes'])
                    ->where('submission_id', $submission->id)
                    ->where('parent_id', NULL)
                    ->orderBy('removed','asc')
                    ->orderBy($sortBy, $direction)
                    ->paginate(200);

然后我在视图中foreach 这些cmets。但是,请注意在我的查询中我只加载父 cmets。

对于儿童,我通过评论的children 关系使用foreach 加载它们。

评论.php:

public function children() {
    return $this->hasMany('App\Comment','parent_id')->orderBy('total_score', 'desc');
}   

comment_block.blade.php:

@foreach ($comment->children as $comment)
    @if ($loop->depth == 10)
        <div>
            <a href="{{ route('get.submission', ['subchan' => $submission->subchan, 'id' => $submission->id, 'URLtitle' => $submission->URL_title,'commentID' => $comment->parent_id]) }}">Continue this thread</a>
        </div>
        @break
    @elseif ($loop->iteration == 8 && $totalComments >= 25) 
        <div class="loadMoreReplies"
        data-submission-id="{{ $submission->id }}"
        data-parent-id="{{$comment->parent_id}}"
        >Load More Replies (<span id="remaining-reply-count-{{$comment->parent_id}}">{{ $loop->remaining + 1 }}</span>)</div>
        @break      
    @else
        <div class="comment-container comment-container-child" id="comment-container-{{$comment->id}}">
            @include('partials.comment_block')
        </div>
    @endif
@endforeach

这样做的问题是,尽管父评论关系被急切加载,但每个子评论也会引起查询,因为它们是通过关系加载到视图中的。因此,我的服务器查询总数从一个增加到可能数百个。

我该如何解决这个问题?

编辑:

对于那些说我没有 n+1 问题的人,这些是我不加​​载子 cmets 时的查询:

这是我加载子 cmets 的时候:

我也使用laravel query detector 包,它肯定告诉我我有一个 n+1 问题。

【问题讨论】:

  • 我可能是错的,但是每个子评论都不会引起新的查询,查询只是一个,当您执行 Comment::with(['children',...]) 时。当您在视图中执行 foreach 时,所有子 cmets 都已存在。
  • 正如@porloscerrosΨ 所说,您没有任何 n+1 问题
  • 他们是肯定的。我在 appserviceprovider 中记录我的查询。对于每个子评论,我都会得到一个额外的查询(在我的例子中是 155)。但是,如果我删除 @foreach ($comment->children as $comment) 块并仅加载父 cmets,我的查询将下降到 1。这就是查询的样子:i.imgur.com/Wvz6Gu3.png
  • @FelixMaxime 你能指出你怀疑刀片视图中的哪一行会执行额外的查询吗?
  • @CaddyDZ 所有子评论关系,但不包括父评论关系。这是我添加子 cmets 时的提醒:i.imgur.com/5INCTqp.png

标签: php laravel


【解决方案1】:

似乎没有办法在 Laravel 的递归中预先加载嵌套关系,或者至少在这种情况下没有(尝试在多个地方询问,而不仅仅是 stackoverflow)。

因此,为了解决我的问题,我将以正常方式加载所有父 cmets 及其关系,但仅将子 cmets 作为数据加载,然后使用 AJAX 调用填充它们。这只会添加一个服务器查询。

【讨论】:

  • 出于好奇,这个问题的答案有帮助吗? stackoverflow.com/questions/26652611/… 这是我在实现同一模型之间的递归时遵循的方法,并进行了一些 QOL 修改。
  • @TimLewis 尝试这个只会给我一个 ErrorException (E_NOTICE) Undefined offset: 0
  • @TimLewis 我确实找到了这个:laracasts.com/discuss/channels/eloquent/… 但我不明白代码
  • 嗯,不知道为什么会这样;这很复杂,但我能够从第一个链接实现解决方案。是的;我不确定第二个链接在做什么;不过看起来很奇怪。 ajax 方法很好。如果您必须进行新调用以获取每个级别的递归,可能不是最流畅的用户体验,但会非常有效。
【解决方案2】:

你的数据库是什么? 如果您的数据库支持公用表表达式,则可以将递归数据优化为单个查询。 但是 Laravel 不支持这些。您必须创建原始查询。

如果您的数据库不支持 CTE(公用表表达式),那么您可以做的最好的事情是分别加载每个级别的递归 @Tim Lewis 对 @Felix Maxime 的评论很好地涵盖了该用例。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2022-01-19
    • 2020-03-08
    • 1970-01-01
    • 2020-08-16
    • 2020-04-08
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多