설명: 이 글은 MySQL 쿼리 수를 줄이기 위해 지연 사전 로딩을 사용하는 Laravel Eloquent의 지연 사전 로딩(Eager Loading)을 주로 설명합니다. 동시에 저자는 읽기 효율성을 높이기 위해 개발 과정에서 일부 스크린샷과 코드를 붙여넣을 예정입니다.
참고: 이제 4개의 테이블이 있습니다: 판매자 테이블, 판매자 전화 테이블 전화, 판매자 소유 상점 상점 테이블 및 상점 테이블 제품의 제품. 그리고 그 관계는
[ 'merchants_phones' => 'one-to-one', 'merchants_shops' => 'one-to-many', 'shops_products' => 'one-to-many', ]
이제 각 매장을 목록으로 표시하는 페이지를 만들어야 합니다. 각 매장 블록에는 제목 등의 매장 정보, 이름, 전화번호 등의 매장 판매자 정보, 소유한 제품 정보가 포함되어 있습니다. 소개와 가격. 사전 로드 유무에 따라 어떤 차이가 있는지 확인하세요.
개발 환경: Laravel5.1+MAMP+PHP7+MySQL5.5
开发环境:Laravel5.1+MAMP+PHP7+MySQL5.5
先写个店铺列表页
1.先装上开发插件三件套
(具体可参考:Laravel学习笔记之Seeder填充数据小技巧)
不管咋样,先装上开发插件三件套:
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
写上表字段和表关联:
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,可以参考Laravel学习笔记之Seeder填充数据小技巧:
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 <meta> <meta> <meta> <!-- 上述3个meta标签*必须*放在最前面,任何其他内容都*必须*跟随其后! --> <title>Bootstrap Template</title> <!-- 新 Bootstrap 核心 CSS 文件 --> <link> <style> html,body{ width: 100%; height: 100%; } *{ margin: 0; border: 0; } </style> <p> </p><p> </p><p> @yield('content') </p> <!-- jQuery文件。务必在bootstrap.min.js 之前引入 --> <script></script> <!-- 最新的 Bootstrap 核心 JavaScript 文件 --> <script></script> <script> </script> //resources/views/shop/index.blade.php @extends('shop.layout') @section('content')
-
@foreach($shops as $shop)
-
Store:{{$shop->name}}
Member:{{$shop->merchant->first_name.' '.$shop->merchant->last_name}} {{--这里数组取电话号码--}} Phone:{{$shop->merchant->phone['number']}}-
@foreach($shop->products as $product)
-
Name:{{$product->name}}
Desc:{{$product->short_desc}}
Price:{{$product->price}}
{{-- {!! Debugbar::info('products:'.$product->id) !!}--}}
@endforeach
@endforeach
-
(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语句上加个排序。截图就不截取了。
总结:关联模型预加载的确是个有意思的功能,效率提高不少。最近都在瞎研究,遇到好玩的东西再分享出来吧,到时见。
스토어 목록 페이지 먼저 작성
1. 먼저 개발 플러그인 3개를 설치합니다. 세트
(자세한 내용은 Laravel 연구 노트의 Seeder로 데이터 채우기 팁을 참조하세요.)무슨 일이 있어도 먼저 3피스 개발 플러그인 세트를 설치하세요: