20.N+1 问题
- 本系列文章为
laracasts.com的系列视频教程——Let's Build A Forum with Laravel and TDD 的学习笔记。若喜欢该系列视频,可去该网站订阅后下载该系列视频, 支持正版 ;- 视频源码地址:github.com/laracasts/Lets-Build-a-...;
- 本项目为一个 forum(论坛)项目,与本站的第二本实战教程 《Laravel 教程 - Web 开发实战进阶》 类似,可互相参照。
本节说明
- 对应视频第 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); } } 此时刷新话题列表页面:
向下滚动会发现很多sql都是类似下面这样的:
select * from `channels` where `channels`.`id` = '1' limit 1 问题出在我们循环使用了 $thread->path() 方法:
. . public function path() { return '/threads/'.$this->channel->slug.'/'.$this->id; } . . 我们将使用 预加载 功能解决这个问题:
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 查询:
如果你仔细观察图片显示的内容,你就会发现:我们使用了两次以下的sql语句:
我们可以看到,是在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); }); } . . 再次刷新页面:
接下来我们来优化话题详情页面的性能问题。访问一个话题详情页面,可以看到:
目前我们存在两个问题需要优化:
- 重复的
select * from users where users.id = '51' limit 1语句; - 获取回复的
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语句数量已大幅减少:
使用预加载功能解决第一个问题:
. . public function replies() { return $this->hasMany(Reply::class) ->withCount('favorites') ->with('owner'); } . . 再次刷新页面:
如果你仔细观察,会发现任然有重复的sql语句,我们将在下一节修复它。
TDD 构建 Laravel 论坛笔记
关于 LearnKu
推荐文章: