懒加载、预加载、with()、load() 傻傻分不清楚?

在本文中,我们将了解 Laravel Eloquent 中的懒加载和预加载以及它如何在后台运行。

Eloquent 模型关系

Factories 工厂表 与 Workers 工人表,是一对多关系。

// Factory.php
class Factory extends Model
{
    public function workers()
    {
        return $this->hasMany(Worker::class);
    }
}

// Worker.php
class Worker extends Model
{
    public function factory()
    {  
        return $this->belongsTo(Factory::class);
    }
}

当我们在控制器中请求时:

public function index()
{
    $factories = Factory::query()->find(1);
} 

此时执行的 SQL 语句只有一条

select * from `factories` where `factories`.`id` = '1'

当我们要访问 Factory 1 的工人时:

public function index()
{
    $factories = Factory::query()->find(1);
    $factories->workers;
} 

此时执行的 SQL 语句是

select * from `factories` where `factories`.`id` = '1'
select * from `workers` where `workers`.`factory_id` = '1' 

产生了一次额外查询。

因为 Eloquent 仅处理了对 Factory 模型进行了查询,并不知道你想要 workers 的关联数据,所以并没有为你准备好它,这样可以避免不必要的查询,来加快返回效率。

Laravel 中对所有模型关联关系的访问,如果没有使用 with() 提前告诉 Eloquent 你想要关联的关系,从而进行访问时,就叫 懒加载。通常也是 N+1 问题经常会出现的地方。

假如我们要访问所有工厂的工人呢?

public function index()
{
    $factories = Factory::query()->get(); // 工厂表有 10 条记录
    foreach($factories as $factory)
    { 
        $factories->workers;
    }
} 

这时就会产生 N+1 的的问题,看一下 SQL 语句

select * from `workers` where `workers`.`factory_id` = '1'
select * from `workers` where `workers`.`factory_id` = '2'
...
select * from `workers` where `workers`.`factory_id` = '9'
select * from `workers` where `workers`.`factory_id` = '10

产生了 10 次 SQL 查询,加上本身对所有工厂的查询,一共 11 次,这就是 N+1 了。


我为什么用 Facotry 工厂表 和 Workers 工人表来举例呢?因为我要用更直白的话语来描述。

工厂晚上 5 点下班,工人们都回宿舍休息了。晚上 10 点时,流水线长突然接到上头指示,来了个急活,需要工人加班来工作。因为工人并不知道晚上要加班,所以都脱衣服上床睡觉了,这个时候线长是不是要把他们挨个都叫起来呀?工人从被窝起来,打着哈欠,一边穿衣服一边嘴里骂骂咧咧,然后回到生产线干活,这个过程就是 `懒加载`

其实 懒加载 并没有什么坏处,它在执行效率上是最优的。程序只查询预期中的的数据,并不知道你要访问它的模型关系,当你需要访问模型关系时,再去查询一次就好了。

但是我们需要注意的是,当查询结果不是一个单条记录(Model),而是多条记录(Collection)时,如果这个时候要去访问 Collection 中每条记录的模型关系,那就需要使用接下来的 预加载 了。否则就会产生上文的 N+1 的问题。


还是刚才的查询,这次使用 with() 预加载工人关系。

public function index()
{
    $factories = Factory::query()->with('workers')->get(); // 工厂表有 10 条记录
    foreach($factories as $factory)
    {
        $factory->workers;
    }
}

此时 SQL 查询就只有 2 条。

select * from `factories`
select * from `workers` where `workers`.`factory_id` in (1, 2, 3, 4, 5, 6, 7, 8, 9, 10)

还是上面的工厂例子,再举一次:

工厂接到一笔新订单,是个急活,厂长周一就通知下去本周六加班,所有工人必须守候在流水线随时待命,这就叫`预加载`

工人信息已经准备好了,只等着你去访问它就可以了。


由此可以得出结论:

当你的查询结果返回的是一个 单条记录(Model),此时 懒加载预加载 其实没有区别,因为每一个模型关系就是一次查询,所以这里不论是使用 with() 预加载,还是直接使用 懒加载,对于单条记录的模型来说,最终 SQL 执行条数都是一样的。

但当你的查询结果返回的是多条记录(Collection) 时,如果要访问模型关系,就必须使用 with() 预加载,否则就会产生 N+1 问题。


那么 load() 是干嘛的?

我们这样查询可以吗?

public function index()
{
    $factory = Factory::query()->load('workers')->find(1);
}

结果是肯定不行的

BadMethodCallException: Call to undefined method Illuminate\Database\Eloquent\Builder::load()

在一个查询没有使用 get()find() 返回之前,它都是一个 EloquentBuilder 对象,我们的所有 where()with()whereIn()、等方法都是在构造查询语句,但其实并没有数据被真正的查询。

当这条语句被执行时,并没有 SQL 语句被执行。

Factory::query()->with('workers');

load() 是模型 Model 才能使用的方法, EloquentBuilder 是不能使用的。


看一下如何使用 load()

public function index()
{
    $factory = Factory::query()->find(1);
    $factory->load('workers'); 
}

此时被执行的 SQL 语句是:

select * from `factories` where `factories`.`id` = '1' limit 1
select * from `workers` where `workers`.`factory_id` in (1)

其实上面的查询和下面的这句 with()是一模一样的:

public function index()
{
    $factory = Factory::query()->with('workers')->find(1); 
}

那么 load() 的使用场景是?

假如我们使用依赖注入的方式来查询 Factory,但同时我们还要把 workers 关联一并返回的时候,就会用到它了。

public function index(Factory $factory)
{ 
    return $factory->load('workers');
}

with() 是查询时一并加载模型关联,load() 是先有模型被查询后,再加载模型的关联时使用的。


希望本篇文章能帮你理清这些概念,如果我有哪里表达不清楚的,还请指正。

enjoy :tada:

本作品采用《CC 协议》,转载必须注明作者和本文链接
悲观者永远正确,乐观者永远前行。
本帖由系统于 2年前 自动加精
《L04 微信小程序从零到发布》
从小程序个人账户申请开始,带你一步步进行开发一个微信小程序,直到提交微信控制台上线发布。
《L02 从零构建论坛系统》
以构建论坛项目 LaraBBS 为线索,展开对 Laravel 框架的全面学习。应用程序架构思路贴近 Laravel 框架的设计哲学。
讨论数量: 10

学到了load的用法 之前一直只用with

2年前 评论

load和with最终的sql都是一样的, load可以随时调用

1年前 评论

讲解的非常好

1年前 评论

分析的非常好

5个月前 评论

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!