0

0

详解Laravel之模型关联预加载

藏色散人

藏色散人

发布时间:2021-04-08 16:10:21

|

4245人浏览过

|

来源于segmentfault

转载

下面由laravel教程栏目给大家介绍laravel之模型关联预加载,希望对需要的朋友有所帮助!

详解Laravel之模型关联预加载

Laravel学习笔记之模型关联预加载

说明:本文主要说明Laravel Eloquent的延迟预加载(Eager Loading),使用延迟预加载来减少MySQL查询次数。同时,作者会将开发过程中的一些截图和代码黏上去,提高阅读效率。

备注:现在有4张表:商家表merchants、商家电话表phones、商家拥有的店铺shops表和店铺里的商品表products。并且关系是:

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

现在要求做出一个页面以列表形式显示每个店铺,每个店铺块包含店铺信息如标题、包含店铺商家信息如姓名和电话、包含拥有的商品信息如介绍和价格。看看有没有预加载会有什么不同。

开发环境: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

表的关系如图:

df49f7bcdf0cecf399a0f9daa27dd12.png

IBM Watson
IBM Watson

IBM Watson文字转语音

下载

然后写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


    
    
    
    
    Bootstrap Template
    
    
    


@yield('content')

//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
@endsection //路由 Route::get('/eagerload', 'ShopController@all');

(2)Debugbar查看程序执行数据
d777dc41ca87ce95aed7da74438ba3c.png

可以看到,执行了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里的查询:
af843e3594f856dc5e66997f968e425.png

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语句上加个排序。截图就不截取了。

总结:关联模型预加载的确是个有意思的功能,效率提高不少。最近都在瞎研究,遇到好玩的东西再分享出来吧,到时见。

相关专题

更多
C++ 高级模板编程与元编程
C++ 高级模板编程与元编程

本专题深入讲解 C++ 中的高级模板编程与元编程技术,涵盖模板特化、SFINAE、模板递归、类型萃取、编译时常量与计算、C++17 的折叠表达式与变长模板参数等。通过多个实际示例,帮助开发者掌握 如何利用 C++ 模板机制编写高效、可扩展的通用代码,并提升代码的灵活性与性能。

10

2026.01.23

php远程文件教程合集
php远程文件教程合集

本专题整合了php远程文件相关教程,阅读专题下面的文章了解更多详细内容。

29

2026.01.22

PHP后端开发相关内容汇总
PHP后端开发相关内容汇总

本专题整合了PHP后端开发相关内容,阅读专题下面的文章了解更多详细内容。

21

2026.01.22

php会话教程合集
php会话教程合集

本专题整合了php会话教程相关合集,阅读专题下面的文章了解更多详细内容。

21

2026.01.22

宝塔PHP8.4相关教程汇总
宝塔PHP8.4相关教程汇总

本专题整合了宝塔PHP8.4相关教程,阅读专题下面的文章了解更多详细内容。

13

2026.01.22

PHP特殊符号教程合集
PHP特殊符号教程合集

本专题整合了PHP特殊符号相关处理方法,阅读专题下面的文章了解更多详细内容。

11

2026.01.22

PHP探针相关教程合集
PHP探针相关教程合集

本专题整合了PHP探针相关教程,阅读专题下面的文章了解更多详细内容。

8

2026.01.22

菜鸟裹裹入口以及教程汇总
菜鸟裹裹入口以及教程汇总

本专题整合了菜鸟裹裹入口地址及教程分享,阅读专题下面的文章了解更多详细内容。

55

2026.01.22

Golang 性能分析与pprof调优实战
Golang 性能分析与pprof调优实战

本专题系统讲解 Golang 应用的性能分析与调优方法,重点覆盖 pprof 的使用方式,包括 CPU、内存、阻塞与 goroutine 分析,火焰图解读,常见性能瓶颈定位思路,以及在真实项目中进行针对性优化的实践技巧。通过案例讲解,帮助开发者掌握 用数据驱动的方式持续提升 Go 程序性能与稳定性。

9

2026.01.22

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
Laravel---API接口
Laravel---API接口

共7课时 | 0.6万人学习

PHP自制框架
PHP自制框架

共8课时 | 0.6万人学习

PHP面向对象基础课程(更新中)
PHP面向对象基础课程(更新中)

共12课时 | 0.7万人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号