Menu

20.N+1 问题

本节说明

  • 对应视频第 20 小节:From 56 Queries Down to 2

本节内容

此时我们的页面存在很大的 性能隐患,为了能更直观地看到问题,我们先安装 Laravel 开发者工具类 - laravel-debugbar。由于我们的 Laravel 为 5.4 版本,所以我们使用以下方式安装:

$ composer require barryvdh/laravel-debugbar:~2.4

安装完成后需要进行注册。我们设置当前环境是本地环境时才开启:
forum\app\Providers\AppServiceProvider.php

public function register()
{
    if($this->app->isLocal()){
        $this->app->register(\Barryvdh\Debugbar\ServiceProvider::class);
    }
}

此时刷新话题列表页面:
file
向下滚动会发现很多sql都是类似下面这样的:

select * from `channels` where `channels`.`id` = '1' limit 1

问题出在导航栏获取channnel时,我们使用了循环语句去获取:

.
.
@ foreach($channels as $channel)
    <li><a href="/threads/{{ $channel->slug }}">{{ $channel->name }}</a> </li>
@endforeach
.
.

我们将使用 预加载 功能解决这个问题:
forum\app\Http\Controllers\ThreadsController.php

.
.
protected function getThreads(Channel $channel, ThreadsFilters $filters)
{
    $threads = Thread::with('channel')->latest()->filter($filters);  -->注意此处

    if ($channel->exists) {
        $threads->where('channel_id', $channel->id);
    }

    $threads = $threads->get();
    return $threads;
}
.
.

方法with()提前加载了我们后面需要用到的关联属性channel,并做了缓存。后面即使是在遍历数据时使用到这个关联属性,数据已经被预加载并缓存,因此不会再产生多余的 SQL 查询:
file
如果你仔细观察图片显示的内容,你就会发现:我们使用了两次以下的sql语句:
file
我们可以看到,是在app\Providers\AppServiceProvider.php文件中发生了两次同样的查询:

.
.
public function boot()
{
    Carbon::setLocale('zh');
    \View::composer('*',function ($view){
       $view->with('channels',Channel::all());
    });
}
.
.

在我们的项目中,chanels属于不会经常变动的数据,所以我们可以选择使用缓存机制来优化:

.
.
public function boot()
{
    Carbon::setLocale('zh');
    \View::composer('*',function ($view){
        $channels = \Cache::rememberForever('channels',function (){
           return Channel::all(); 
        });
       $view->with('channels',$channels);
    });
}
.
.

再次刷新页面:
file
接下来我们来优化话题详情页面的性能问题。访问一个话题详情页面,可以看到:
file
目前我们存在两个问题需要优化:

  1. 重复的select * from users where users.id = '51' limit 1语句;
  2. 获取回复的count(*)语句

首先我们看一下详情页面回复区域的代码:
forum\resources\views\threads\reply.blade.php

<div class="panel panel-default">
    <div class="panel-heading">
        <div class="level">
            <h5 class="flex">
                <a href="#"> {{ $reply->owner->name }}</a>
                回复于
                {{ $reply->created_at->diffForHumans() }}
            </h5>

            <div>
                <form method="POST" action="/replies/{{ $reply->id }}/favorites">
                    {{ csrf_field() }}

                    <button type="submit" class="btn btn-default" {{ $reply->isFavorited() ? 'disabled' : '' }}>
                        {{ $reply->favorites()->count() }} {{ str_plural('Favorite',$reply->favorites()->count()) }}
                    </button>
                </form>
            </div>
        </div>
    </div>

    <div class="panel-body">
        {{ $reply->body }}
    </div>
</div>

导致第一个问题的代码:

$reply->owner->name

导致第二个问题的代码:

{{ $reply->favorites()->count() }} {{ str_plural('Favorite',$reply->favorites()->count()) }}

我们可以利用模型关联的 关联数据计数 功能,使用withCount方法,此方法会在结果集中增加一个favorites_count字段:
forum\app\Thread.php

.
.
public function replies()
{
    return $this->hasMany(Reply::class)
        ->withCount('favorites');
}
.
.

在页面应用:

.
.
<form method="POST" action="/replies/{{ $reply->id }}/favorites">
    {{ csrf_field() }}

    <button type="submit" class="btn btn-default" {{ $reply->isFavorited() ? 'disabled' : '' }}>
        {{ $reply->favorites_count }} {{ str_plural('Favorite',$reply->favorites_count) }}
    </button>
</form>
.
.

再次刷新页面,可以看到sql语句数量已大幅减少:
file
使用预加载功能解决第一个问题:

.
.
public function replies()
{
    return $this->hasMany(Reply::class)
        ->withCount('favorites')
        ->with('owner');
}
.
.

再次刷新页面:
file
如果你仔细观察,会发现任然有重复的sql语句,我们将在下一节修复它。

本文章首发在 Laravel China 社区
上一篇 下一篇
刻意练习,每日精进。
4
点赞
321
浏览
1
讨论

维护者
18
18