Cal Huang

  • 首页
  • About Me
Cal Huang
  1. 首页
  2. Laravel
  3. 正文

翻译:在 Laravel 5 中使用 Repository 模式 (2)关联关系和渴求式加载

2016年11月10日 6429点热度 1人点赞 0条评论

和上一篇一样 也是一篇翻译的文章,原文在这里 ,原作者: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 问题。为了解决这个问题,我们可以使用渴求式加载。

什么是渴求式加载

简单地说,渴求式加载就是在一开始的时候就把一切都准备好。懒惰式加载与之相反。懒惰式加载是等需要用到的时候才去准备。渴求式加载可以帮助我们避开性能陷阱。可能有个例子会更好理解,想象一下下面的这个情况:

stores_model

我们有三个关连的模型。他们之间的关系可以这样描述,每个 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 。

storelist_mockup

 

在 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

 

结果是:

n_plus_one_problem

在这个例子里 ,我在数据库里放置了 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 个查询:

n_plus_one_problem_eager

即使你有 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 个查询:

eager_loading

结论

正确使用渴求式加载可以大幅提高应用的性能。但是有时候,使用渴求式加载还是不够用。下一个教程里,我将演示如何使用缓存来获取更高的性能。

标签: 暂无
最后更新:2016年11月10日

Cal Huang

这个人很懒,什么都没留下

点赞
< 上一篇

文章评论

razz evil exclaim smile redface biggrin eek confused idea lol mad twisted rolleyes wink cool arrow neutral cry mrgreen drooling persevering
取消回复

COPYRIGHT © 2021 hhyhhy.com. ALL RIGHTS RESERVED.

Theme Kratos Made By Seaton Jiang