Laravel5.2之模型關聯預加載

說明:本文主要說明Laravel Eloquent的延遲預加載(Eager Loading),使用延遲預加載來減小MySQL查詢次數。同時,會將開發過程當中的一些截圖和代碼黏上去,提升閱讀效率。php

備註:如今有4張表:商家表merchants、商家電話表phones、商家擁有的店鋪shops表和店鋪裏的商品表products。而且關係是:css

[
    'merchants_phones' => 'one-to-one',
    'merchants_shops'  => 'one-to-many',
    'shops_products'   => 'one-to-many',
]

如今要求作出一個頁面以列表形式顯示每一個店鋪,每一個店鋪塊包含店鋪信息如標題、包含店鋪商家信息如姓名和電話、包含擁有的商品信息如介紹和價格。看看有沒有預加載會有什麼不一樣。html

開發環境:Laravel5.1+MAMP+PHP7+MySQL5.5jquery

先寫個店鋪列表頁

1.先裝上開發插件三件套(具體可參考:Laravel5.2之Seeder填充數據小技巧)
無論咋樣,先裝上開發插件三件套:laravel

composer require barryvdh/laravel-debugbar --dev
composer require barryvdh/laravel-ide-helper --dev
composer require mpociot/laravel-test-factory-helper --dev

//config/app.php
/** *Develop Plugin */        
Barryvdh\Debugbar\ServiceProvider::class,
Mpociot\LaravelTestFactoryHelper\TestFactoryHelperServiceProvider::class,
Barryvdh\LaravelIdeHelper\IdeHelperServiceProvider::class,

2.寫上表字段、表關聯和測試數據填充器Seeder
依次輸入指令:編程

php artisan make:model Merchant -m
php artisan make:model Phone -m
php artisan make:model Shop -m
php artisan make:model Product -m

寫上表字段和表關聯:bootstrap

class CreateMerchantsTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('merchants', function (Blueprint $table) {
            $table->increments('id');
            $table->string('username')->unique();
            $table->string('email')->unique();
            $table->string('first_name');
            $table->string('last_name');
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::drop('merchants');
    }
}

class CreatePhonesTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('phones', function (Blueprint $table) {
            $table->increments('id');
            $table->integer('number')->unsigned();
            $table->integer('merchant_id')->unsigned();
            $table->timestamps();
            $table->foreign('merchant_id')
                ->references('id')
                ->on('merchants')
                ->onUpdate('cascade')
                ->onDelete('cascade');
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::table('phones', function($table){
            $table->dropForeign('merchant_id'); // Drop foreign key 'user_id' from 'posts' table
        });
        Schema::drop('phones');
    }
}

class CreateShopsTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('shops', function (Blueprint $table) {
            $table->increments('id');
            $table->string('name');
            $table->string('slug')->unique();
            $table->string('site');
            $table->integer('merchant_id')->unsigned();
            $table->timestamps();
            $table->foreign('merchant_id')
                ->references('id')
                ->on('merchants')
                ->onUpdate('cascade')
                ->onDelete('cascade');
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::table('shops', function($table){
            $table->dropForeign('merchant_id'); // Drop foreign key 'user_id' from 'posts' table
        });
        Schema::drop('shops');
    }
}

class CreateProductsTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('products', function (Blueprint $table) {
            $table->increments('id');
            $table->string('name');
            $table->text('short_desc');
            $table->text('long_desc');
            $table->double('price');
            $table->integer('shop_id')->unsigned();
            $table->timestamps();
            $table->foreign('shop_id')
                ->references('id')
                ->on('shops')
                ->onUpdate('cascade')
                ->onDelete('cascade');
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::table('products', function($table){
            $table->dropForeign('shop_id'); // Drop foreign key 'user_id' from 'posts' table
        });
        Schema::drop('products');
    }
}

/**
 * App\Merchant
 *
 * @property integer $id
 * @property string $username
 * @property string $email
 * @property string $first_name
 * @property string $last_name
 * @property \Carbon\Carbon $created_at
 * @property \Carbon\Carbon $updated_at
 * @property-read \App\Phone $phone
 * @property-read \Illuminate\Database\Eloquent\Collection|\App\Shop[] $shops
 * @method static \Illuminate\Database\Query\Builder|\App\Merchant whereId($value)
 * @method static \Illuminate\Database\Query\Builder|\App\Merchant whereUsername($value)
 * @method static \Illuminate\Database\Query\Builder|\App\Merchant whereEmail($value)
 * @method static \Illuminate\Database\Query\Builder|\App\Merchant whereFirstName($value)
 * @method static \Illuminate\Database\Query\Builder|\App\Merchant whereLastName($value)
 * @method static \Illuminate\Database\Query\Builder|\App\Merchant whereCreatedAt($value)
 * @method static \Illuminate\Database\Query\Builder|\App\Merchant whereUpdatedAt($value)
 * @mixin \Eloquent
 */
class Merchant extends Model
{
    /**
     * @return \Illuminate\Database\Eloquent\Relations\HasOne
     */
    public function phone()
    {
        return $this->hasOne(Phone::class, 'merchant_id');
    }

    /**
     * @return \Illuminate\Database\Eloquent\Relations\HasMany
     */
    public function shops()
    {
        return $this->hasMany(Shop::class, 'merchant_id');
    }
}

/**
 * App\Phone
 *
 * @property integer $id
 * @property integer $number
 * @property integer $merchant_id
 * @property \Carbon\Carbon $created_at
 * @property \Carbon\Carbon $updated_at
 * @property-read \App\Merchant $merchant
 * @method static \Illuminate\Database\Query\Builder|\App\Phone whereId($value)
 * @method static \Illuminate\Database\Query\Builder|\App\Phone whereNumber($value)
 * @method static \Illuminate\Database\Query\Builder|\App\Phone whereMerchantId($value)
 * @method static \Illuminate\Database\Query\Builder|\App\Phone whereCreatedAt($value)
 * @method static \Illuminate\Database\Query\Builder|\App\Phone whereUpdatedAt($value)
 * @mixin \Eloquent
 */
class Phone extends Model
{
    /**
     * @return \Illuminate\Database\Eloquent\Relations\BelongsTo
     */
    public function merchant()
    {
        return $this->belongsTo(Merchant::class, 'merchant_id');
    }
}

/**
 * App\Product
 *
 * @property integer $id
 * @property string $name
 * @property string $short_desc
 * @property string $long_desc
 * @property float $price
 * @property integer $shop_id
 * @property \Carbon\Carbon $created_at
 * @property \Carbon\Carbon $updated_at
 * @property-read \Illuminate\Database\Eloquent\Collection|\App\Shop[] $shop
 * @method static \Illuminate\Database\Query\Builder|\App\Product whereId($value)
 * @method static \Illuminate\Database\Query\Builder|\App\Product whereName($value)
 * @method static \Illuminate\Database\Query\Builder|\App\Product whereShortDesc($value)
 * @method static \Illuminate\Database\Query\Builder|\App\Product whereLongDesc($value)
 * @method static \Illuminate\Database\Query\Builder|\App\Product wherePrice($value)
 * @method static \Illuminate\Database\Query\Builder|\App\Product whereShopId($value)
 * @method static \Illuminate\Database\Query\Builder|\App\Product whereCreatedAt($value)
 * @method static \Illuminate\Database\Query\Builder|\App\Product whereUpdatedAt($value)
 * @mixin \Eloquent
 */
class Product extends Model
{
    /**
     * @return \Illuminate\Database\Eloquent\Relations\BelongsTo
     */
    public function shop()
    {
        return $this->belongsTo(Shop::class, 'shop_id');
    }
}

/**
 * App\Shop
 *
 * @property integer $id
 * @property string $name
 * @property string $slug
 * @property string $site
 * @property integer $merchant_id
 * @property \Carbon\Carbon $created_at
 * @property \Carbon\Carbon $updated_at
 * @property-read \Illuminate\Database\Eloquent\Collection|\App\Merchant[] $merchant
 * @property-read \Illuminate\Database\Eloquent\Collection|\App\Product[] $products
 * @method static \Illuminate\Database\Query\Builder|\App\Shop whereId($value)
 * @method static \Illuminate\Database\Query\Builder|\App\Shop whereName($value)
 * @method static \Illuminate\Database\Query\Builder|\App\Shop whereSlug($value)
 * @method static \Illuminate\Database\Query\Builder|\App\Shop whereSite($value)
 * @method static \Illuminate\Database\Query\Builder|\App\Shop whereMerchantId($value)
 * @method static \Illuminate\Database\Query\Builder|\App\Shop whereCreatedAt($value)
 * @method static \Illuminate\Database\Query\Builder|\App\Shop whereUpdatedAt($value)
 * @mixin \Eloquent
 */
class Shop extends Model
{
    /**
     * @return \Illuminate\Database\Eloquent\Relations\BelongsTo
     */
    public function merchant()
    {
        return $this->belongsTo(Merchant::class, 'merchant_id');
    }

    /**
     * @return \Illuminate\Database\Eloquent\Relations\HasMany
     */
    public function products()
    {
        return $this->hasMany(Product::class, 'shop_id');
    }
}

別忘了利用下開發三件套輸入指令:設計模式

php artisan ide-helper:generate
php artisan ide-helper:models
php artisan test-factory-helper:generate

表的關係如圖:數組

而後寫Seeder,能夠參考Laravel5.2之Seeder填充數據小技巧:app

php artisan make:seeder MerchantTableSeeder
php artisan make:seeder PhoneTableSeeder
php artisan make:seeder ShopTableSeeder
php artisan make:seeder ProductTableSeeder
class MerchantTableSeeder extends Seeder
{
    /** * Run the database seeds. * * @return void */
    public function run()
    {
        $faker = Faker\Factory::create();
        $datas = [];
        foreach (range(1, 20) as $key => $value) {
            $datas[] = [
                'username'   =>  $faker->userName ,
                'email'      =>  $faker->safeEmail ,
                'first_name' =>  $faker->firstName ,
                'last_name'  =>  $faker->lastName ,
                'created_at' => \Carbon\Carbon::now()->toDateTimeString(),
                'updated_at' => \Carbon\Carbon::now()->toDateTimeString()
            ];
        }

        DB::table('merchants')->insert($datas);
    }
}

class PhoneTableSeeder extends Seeder
{
    /** * Run the database seeds. * * @return void */
    public function run()
    {
        $faker        = Faker\Factory::create();
        $merchant_ids = \App\Merchant::lists('id')->toArray();
        $datas        = [];
        foreach (range(1, 20) as $key => $value) {
            $datas[]  = [
                'number'      => $faker->randomNumber() ,
                'merchant_id' => $faker->randomElement($merchant_ids) ,
                'created_at'  => \Carbon\Carbon::now()->toDateTimeString(),
                'updated_at'  => \Carbon\Carbon::now()->toDateTimeString()
            ];
        }

        DB::table('phones')->insert($datas);
    }
}

class ShopTableSeeder extends Seeder
{
    /** * Run the database seeds. * * @return void */
    public function run()
    {
        $faker        = Faker\Factory::create();
        $merchant_ids = \App\Merchant::lists('id')->toArray();
        $datas        = [];
        foreach (range(1, 40) as $key => $value) {
            $datas[]  = [
                'name'         =>  $faker->name ,
                'slug'         =>  $faker->slug ,
                'site'         =>  $faker->word ,
                'merchant_id'  =>  $faker->randomElement($merchant_ids) ,
                'created_at'   => \Carbon\Carbon::now()->toDateTimeString(),
                'updated_at'   => \Carbon\Carbon::now()->toDateTimeString()
            ];
        }

        DB::table('shops')->insert($datas);
    }
}

class ProductTableSeeder extends Seeder
{
    /** * Run the database seeds. * * @return void */
    public function run()
    {
        $faker    = Faker\Factory::create();
        $shop_ids = \App\Shop::lists('id')->toArray();
        $datas    = [];
        foreach (range(1, 30) as $key => $value) {
            $datas[] = [
                'name'              =>  $faker->name ,
                'short_desc'        =>  $faker->text ,
                'long_desc'         =>  $faker->text ,
                'price'             =>  $faker->randomFloat() ,
                'shop_id'           =>  $faker->randomElement($shop_ids) ,
                'created_at'        =>  \Carbon\Carbon::now()->toDateTimeString() ,
                'updated_at'        =>  \Carbon\Carbon::now()->toDateTimeString()
            ];
        }

        DB::table('products')->insert($datas);
    }
}

php artisan db:seed

3.寫個簡單View視圖
(1)用Repository Pattern來組織代碼

//app/Repository
namespace App\Repository;
interface ShopRepositoryInterface
{
    public function all();
}
//app/Repository/Eloquent
namespace App\Repository\Eloquent;

use App\Repository\ShopRepositoryInterface;
use App\Shop;

class ShopRepository implements ShopRepositoryInterface
{
    /**
     * @var Shop
     */
    public $shop;
    public function __construct(Shop $shop)
    {
        $this->shop = $shop;
    }

    public function all()
    {
        // TODO: Implement all() method.
        $shops = $this->shop->all();
        return $shops;
    }
}
//app/provider/ShopRepositoryServiceProvider
//php artisan make:provider ShopRepositoryServiceProvider
/**
     * Register the application services.
     *
     * @return void
     */
    public function register()
    {
        $this->app->bind(ShopRepositoryInterface::class, ShopRepository::class);
    }
    
//app/Http/Controllers/ShopController.php
class ShopController extends Controller
{
    /**
     * @var ShopRepositoryInterface
     */
    public $shop;

    /**
     * ShopController constructor.
     * @param ShopRepositoryInterface $shopRepositoryInterface
     */
    public function __construct(ShopRepositoryInterface $shopRepositoryInterface)
    {
        $this->shop = $shopRepositoryInterface;
    }

    public function all()
    {
        $shops = $this->shop->all();
        return view('shop.index', compact('shops'));
    }
}

//視圖
//resources/views/shop/layout.blade.php
<html lang="en">
<head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <!-- 上述3個meta標籤*必須*放在最前面,任何其餘內容都*必須*跟隨其後! -->
    <title>Bootstrap Template</title>
    <!-- 新 Bootstrap 核心 CSS 文件 -->
    <link rel="stylesheet" href="//cdn.bootcss.com/bootstrap/3.3.5/css/bootstrap.min.css">
    <style>
        html,body{
            width: 100%;
            height: 100%;
        }
        *{
            margin: 0;
            border: 0;
        }
    </style>
</head>
<body>
<div class="container">
    <div class="row">
        <div class="col-xs-12 col-md-12">

            @yield('content')

        </div>
    </div>
</div>

<!-- jQuery文件。務必在bootstrap.min.js 以前引入 -->
<script src="//cdn.bootcss.com/jquery/1.11.3/jquery.min.js"></script>
<!-- 最新的 Bootstrap 核心 JavaScript 文件 -->
<script src="//cdn.bootcss.com/bootstrap/3.3.5/js/bootstrap.min.js"></script>
<script>

</script>
</body>
</html>

//resources/views/shop/index.blade.php
@extends('shop.layout')

@section('content')
    <ul class="list-group">
        @foreach($shops as $shop)
            <li class="list-group-item" style="margin-top: 10px">
                <h1><strong style="color: darkred">Store:</strong>{{$shop->name}}</h1>
                <span><strong style="color: orangered">Member:</strong>{{$shop->merchant->first_name.' '.$shop->merchant->last_name}}</span>
                {{--這裏數組取電話號碼--}}
                <span><strong style="color: orangered">Phone:</strong>{{$shop->merchant->phone['number']}}</span>
                <ul class="list-group">
                    @foreach($shop->products as $product)
                        <li class="list-group-item">
                            <h3><strong style="color: red">Name:</strong>{{$product->name}}</h3>
                            <h4><strong style="color: red">Desc:</strong>{{$product->short_desc}}</h4>
                            <h4><strong style="color: red">Price:</strong>{{$product->price}}</h4>

{{--                            {!! Debugbar::info('products:'.$product->id) !!}--}}
                        </li>
                    @endforeach
                </ul>
            </li>
        @endforeach
    </ul>

@endsection

//路由
Route::get('/eagerload', 'ShopController@all');

(2)Debugbar查看程序執行數據

能夠看到,執行了121次query,耗時38.89ms,效率很低,仔細觀察每個statement就發現這是先掃描shops表,再根據shops中每個merchant_id去查找merchants表,查找products表也是這樣,又有不少次query,這是N+1查找問題。

預加載查詢

(1)嵌套預加載
Eloquent在經過屬性訪問關聯數據時是延遲加載的,就是隻有該關聯數據只有在經過屬性訪問它時纔會被加載。在查找上層模型時能夠經過預加載關聯數據,避免N+1問題。並且,使用預加載超級簡單。
只需修改一行:

//app/Repository/Eloquent/ShopRepository
    public function all()
    {
        // TODO: Implement all() method.
// $shops = $this->shop->all();
        //經過`點`語法嵌套預加載,多種關聯就寫對應的關聯方法
        //Shop這個Model裏關聯方法是Merchant()和Products(),Merchant Model裏關聯方法是Phone()
        $shops = $this->shop->with(['merchant.phone', 'products'])->get();
        return $shops;
    }

不須要修改其餘代碼,再看Debugbar裏的查詢:

It is working!!!

發現:只有4個query,耗時3.58ms,效率提升不少。把原來的N+1這種query改形成了where..in..這種query,效率提升很多。能夠用EXPLAIN來查看SQL語句的執行計劃。

(2)預加載條件限制
還能夠對預加載進行條件限制,如對products進行預先排序,代碼也很好修改,只需:

//app/Repository/Eloquent/ShopRepository
public function all()
    {
        // TODO: Implement all() method.
// $shops = $this->shop->all();
// $shops = $this->shop->with(['merchant.phone', 'products'])->get();
        $shops = $this->shop->with(['members.phone', 'products'=>function($query){
// $query->orderBy('price', 'desc');
            $query->orderBy('price', 'asc');
        }])->get();
        return $shops;
    }

經過加個限制條件,就等於在預加載products時SQL語句上加個排序。截圖就不截取了。

昨天看到有個問題是問Laravel值不值得花時間玩一玩,我的自學Laravel有幾個月了,玩PHP也11個月,開始發現PHP的代碼很是麪條式,但後來瞭解到Laravel和Composer後,發現PHP原來很強大的,我的建議Laravel是值得花時間玩一玩的,也推薦一本書《Modern PHP》。其實寫代碼須要些不少東西的,而不單單面條式業務邏輯,好比用PHPUnit寫單元測試和集成測試,各類設計模式,SOLID原則,依賴注入和控制反轉的容器等等,這些東西我的都是玩Laravel後才瞭解並學習了一些,就我的而言,玩Laravel後學到了不少代碼技巧,感受到編程是一項技術活,不是體力活。

總結:關聯模型預加載的確是個有意思的功能,效率提升很多。最近都在瞎研究,遇到好玩的東西再分享出來吧,到時見。

相關文章
相關標籤/搜索