和上一篇一样 也是一篇翻译的文章,原文在这里 ,原作者:Mirza Pasic。
前言
开始这篇文章之前,我们先聊聊我们可能需要面对的问题。最近,有个客户抱怨页面打开非常慢。我决定打开 debug 信息来看看。其中 Query 部分令我非常吃惊,显示页面竟然用了 16500+ 个查询。
检查了一下。我找到了问题的源头—— 3 个 foreach 循环。它们通过 Model 里定义的关联关系来获取一些属性。它本来工作得非常正常,直到数据库里有 大约 5500 条数据。代码如下:
$main_object = MainObject::all(); foreach($main_object as $object) { echo $object->some_property; foreach($object->related_object as $related) { echo $related->some_property; echo $related->another_property; } foreach($object->another_related as $another) { echo $another->some_property; echo $another->another_property; } }
如果,$main_object = MainObject::all(); 返回了 5500 条数据 ,第一个 foreach 将会运行 5500 次,第二个 foreach 也会运行 5500 次。使用 ORM 经常会使开发人员写出非常低效的 ,消耗非常大的代码,而 ORM 另这些错误难以被发现。上面的提到的这个问题被称为 N + 1 问题。为了解决这个问题,我们可以使用渴求式加载。
什么是渴求式加载
简单地说,渴求式加载就是在一开始的时候就把一切都准备好。懒惰式加载与之相反。懒惰式加载是等需要用到的时候才去准备。渴求式加载可以帮助我们避开性能陷阱。可能有个例子会更好理解,想象一下下面的这个情况:
我们有三个关连的模型。他们之间的关系可以这样描述,每个 member 可以拥有多个 store ,但是每个 store 只能属于一个 member 。每个 store 可以用个多个 product 但是每个 product 只能属于一个 store 。
下面是它们三个的 Eloquent model 文件:
Member:
<?php namespace App; use Illuminate\Database\Eloquent\Model; class Member extends Model { /** * The attributes that are mass assignable. * * @var array */ protected $fillable = ['username', 'email', 'first_name', 'last_name']; public function stores() { return $this->hasMany('App\\Store'); } }
Store:
<?php namespace App; use Illuminate\Database\Eloquent\Model; class Store extends Model { /** * The attributes that are mass assignable. * * @var array */ protected $fillable = ['name', 'slug', 'site', 'member_id']; public function member() { return $this->belongsTo('App\\Member'); } public function products() { return $this->hasMany('App\\Product'); } }
Product:
<?php namespace App; use Illuminate\Database\Eloquent\Model; class Product extends Model { /** * The attributes that are mass assignable. * * @var array */ protected $fillable = ['name', 'short_desc', 'long_desc', 'price', 'store_id', 'member_id']; public function store() { return $this->belongsTo('App\\Store'); } }
假设一个这样的场景,构建一个应用,这个应用允许用户构建自己的 store ,当然,每个 store 里又可以插入 许多的 product 。还需要一个页面来显示所有的 store 和 store 里的主要的 product 。
在 controller 里你可能会这样写:
<?php namespace App\Http\Controllers; use App\Repositories\StoreRepository; class StoresController extends Controller { protected $stores; function __construct(StoreRepository $stores) { $this->stores = $stores; } public function index() { $stores = $this->stores->all(); return \View::make('stores.index')->with('stores', $stores); } }
在 view 里你可能是这么写的:
@foreach($stores as $store) <h1>{{ $store->name }}</h1> <span>Owner: {{ $store->member->first_name . ' ' . $store->member->last_name }}</span><br> <h2>Products:</h2> @foreach($store->products as $product) <h3>{{$product->name}}</h3> <span>{{$product->short_desc}}</span><br/><br/> <span>Price: {{$product->price}}</span> <br/> <?php Debugbar::info('Product displayed'); ?> @endforeach <br/> ======================== <br/> @endforeach
结果是:
在这个例子里 ,我在数据库里放置了 5 个 member ,3 个 store ,和 4 个 product 。第一个查询就是从数据库里获取 所有的 store ,这也是 N + 1 问题的一部分。在这个例子 N 就是指第一个查询里返回的 store 的数量。我们有 3 个 store 那么我们还要查询 member 3 次,在 product 里再查询 3 次。那么总共就是 3 + 3 + 1 次查询。
想象一下,如果我们有 5000 或者 10000 个 store 呢,每当用户访问的时候,就可能要执行 10-20k 次查询。如果你每天有 10k 或者 100k 的 PV 。那简直就是一个噩梦。现在非常清楚了,这种方式就是一个性能杀手。无论使用什么 数据库,多么强大的服务器,这些方法都会造成很大的问题。你可能想用缓存来提高性能,比如 Redis 。它只能暂时撑一阵子。不过这会花费你很多的钱和时间,在此期间,你可能会失去很多的用户。
渴求式加载可破。在 Laravel 中使用渴求式加载非常的简单,你只需要在查询上使用 with 方法:
$stores = Store::with('member','products')->get();
现在,你可以发现有了非常大的改善只用了 3 个查询:
即使你有 10k 个 store 也只用 3 个查询,渴求式加载是非常有用的东西。正确使用它可以大幅提高程序的性能。当然了,我们应该有一个 id 字段并且这个字段建立了正确的索引,在没有建立索引的字段上执行 where in 查询也会消耗很多时间。
在介绍玩了 渴求式加载之后,可以看看在 Repository 模式里怎么使用。
扩展 Repostory 类
我会向你展示在具体的 Repostory 类中怎么使用关联关系。这有个例子:
function __construct(StoreRepository $stores) { $this->stores = $stores; } public function index() { $stores = $this->stores->with('member', 'products')->all(); .... }
我们将会有个办法使用 Model 里定义的关联关系,这个方法类似于 Laravel’s Query Builder with 方法。
public function with($relations) { if (is_string($relations)) $relations = func_get_args(); $this->with = $relations; return $this; }
然后我们需要把这些关联关系链接到 Model 上:
protected function eagerLoadRelations() { if(!is_null($this->with)) { foreach ($this->with as $relation) { $this->model->with($relation); } } return $this; }
最后,我们需要修改一下我们的 all() 函数:
public function all($columns = array('*')) { $this->applyCriteria(); $this->newQuery()->eagerLoadRelations(); return $this->model->get($columns); }
好了,那么现在我们来看看 controller :
<?php namespace App\Http\Controllers; use App\Repositories\StoreRepository; class StoresController extends Controller { protected $stores; function __construct(StoreRepository $stores) { $this->stores = $stores; } public function index() { $stores = $this->stores->with('member', 'products')->all(); return \View::make('stores.index')->with('stores', $stores); } }
然后 在 view 里你就可以尽情的显示你想显示的数据了。
@foreach($stores as $store) <h1>{{ $store->name }}</h1> <span>Owner: {{ $store->member->first_name . ' ' . $store->member->last_name }}</span><br> <h2>Products:</h2> @foreach($store->products as $product) <h3>{{$product->name}}</h3> <span>{{$product->short_desc}}</span><br/><br/> <span>Price: {{$product->price}}</span> <br/> <?php Debugbar::info('Product displayed'); ?> @endforeach <br/> ======================== <br/> @endforeach
同样的,只用 3 个查询:
结论
正确使用渴求式加载可以大幅提高应用的性能。但是有时候,使用渴求式加载还是不够用。下一个教程里,我将演示如何使用缓存来获取更高的性能。
文章评论