页面调优
此刻我们的页面存在很大的 性能隐患,为了能更直观地看到问题,我们先安装 Laravel 开发者工具类 - laravel-debugbar。
安装 Debugbar
使用 Composer 安装:
$ composer require "barryvdh/laravel-debugbar:~3.2" --dev
刷新列表页面即可看到我们的开发者工具栏:
N +1 问题
如图点击以下按钮,可看到整个页面执行了 33 条查询语句,往下滚动可以看到很多请求都是重复的:
Laravel 的默认分页是 15 条信息,如果我们在控制器中修改 paginate(30) 显示条目为 30 的话:
app/Http/Controllers/TopicsController.php
<?php
.
.
.
class TopicsController extends Controller
{
.
.
.
public function index()
{
$topics = Topic::paginate(30);
return view('topics.index', compact('topics'));
}
.
.
.
}
可以看到现在的 SQL 查询数量为 63,是之前的两倍:
63 statements were executed, 60 of which were duplicated, 3 unique
意为:总共有 63 条语句执行了,其中 60 条是重复的。
以上的问题就是 N+1 问题,不仅是 Laravel 中,所有的 ORM 关联数据读取中都存在此问题,新手很容易踩到坑。进而导致系统变慢,然后拖垮整个系统。
N+1 一般发生在关联数据的遍历时。在 resources/views/topics/_topic_list.blade.php 模板中,我们对$topics 进行遍历,为了方便解说,我们将此文件里的代码精简为如下:
@if (count($topics))
<ul class="list-unstyled">
@foreach ($topics as $topic)
<li class="media">
<div class="media-left">
<a href="{{ route('users.show', [$topic->user_id]) }}">
<img class="media-object img-thumbnail mr-3" style="width: 52px; height: 52px;"
src="{{ $topic->user->avatar }}" title="{{ $topic->user->name }}">
</a>
</div>
<div class="media-body">
<div class="media-heading mt-0 mb-1">
<a href="{{ route('topics.show', [$topic->id]) }}" title="{{ $topic->title }}">
{{ $topic->title }}
</a>
<a class="float-right" href="{{ route('topics.show', [$topic->id]) }}">
<span class="badge badge-secondary badge-pill"> {{ $topic->reply_count }} </span>
</a>
</div>
<small class="media-body meta text-secondary">
<a class="text-secondary" href="#" title="{{ $topic->category->name }}">
<i class="far fa-folder"></i>
{{ $topic->category->name }}
</a>
<span> • </span>
<a class="text-secondary" href="{{ route('users.show', [$topic->user_id]) }}"
title="{{ $topic->user->name }}">
<i class="far fa-user"></i>
{{ $topic->category->name }}
</a>
<span> • </span>
<i class="far fa-clock"></i>
<span class="timeago"
title="最后活跃于:{{ $topic->updated_at }}">{{ $topic->updated_at->diffForHumans() }}</span>
</small>
</div>
</li>
@if ( ! $loop->last)
<hr>
@endif
@endforeach
</ul>
@else
<div class="empty-block">暂无数据 ~_~</div>
@endif
为了读取 user 和 category ,每次的循环都要查一下 users 和 categories 表,在本例子中我们查询了 30条话题数据,那么最终我需要执行的查询语句就是 30 * 2 + 1 = 61 条语句。如果我第一次查询出来的是 N 条记录,那么最终需要执行的 SQL 语句就是 N+1 次。
如何解决 N + 1 问题?
我们可以通过 Eloquent 提供的 预加载 来解决此问题:
app/Http/Controllers/TopicsController.php
public function index()
{
$topics = Topic::with('user', 'category')->paginate(30);
return view('topics.index', compact('topics'));
}
方法 with() 提前加载了我们后面需要用到的关联属性 user 和 category ,并做了缓存。后面即使是在遍历数据时使用到这两个关联属性,数据已经被预加载并缓存,因此不会再产生多余的 SQL 查询:
上图可以看到优化完成后,我们的 SQL 查询数量瞬间减少到只有 5 条,相应的,页面的响应时间也减少了三分之一。