🔥码云GVP开源项目 12k star Uniapp+ElementUI 功能强大 支持多语言、二开方便! 广告
##### Eloquent 模型关系 `Factories`工厂表 与`Workers`工人表,是一对多关系。 ~~~php // 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); } } ~~~ 当我们在控制器中请求时: ~~~php public function index() { $factories = Factory::query()->find(1); } ~~~ 此时执行的 SQL 语句只有一条 ~~~php select * from `factories` where `factories`.`id` = '1' ~~~ 当我们要访问 Factory 1 的工人时: ~~~php public function index() { $factories = Factory::query()->find(1); $factories->workers; } ~~~ 此时执行的 SQL 语句是 ~~~php select * from `factories` where `factories`.`id` = '1' select * from `workers` where `workers`.`factory_id` = '1' ~~~ 产生了一次额外查询。 因为 Eloquent 仅处理了对`Factory`模型进行了查询,并不知道你想要`workers`的关联数据,所以并没有为你准备好它,这样可以避免不必要的查询,来加快返回效率。 Laravel 中对所有模型关联关系的访问,如果没有使用`with()`提前告诉 Eloquent 你想要关联的关系,从而进行访问时,就叫`懒加载`。通常也是`N+1`问题经常会出现的地方。 #### 假如我们要访问所有工厂的工人呢? ~~~php public function index() { $factories = Factory::query()->get(); // 工厂表有 10 条记录 foreach($factories as $factory) { $factories->workers; } } ~~~ 这时就会产生`N+1`的的问题,看一下 SQL 语句 ~~~php 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`工人表来举例呢?因为我要用更直白的话语来描述。 ~~~php 工厂晚上 5 点下班,工人们都回宿舍休息了。晚上 10 点时,流水线长突然接到上头指示,来了个急活,需要工人加班来工作。因为工人并不知道晚上要加班,所以都脱衣服上床睡觉了,这个时候线长是不是要把他们挨个都叫起来呀?工人从被窝起来,打着哈欠,一边穿衣服一边嘴里骂骂咧咧,然后回到生产线干活,这个过程就是 `懒加载`。 ~~~ 其实`懒加载`并没有什么坏处,它在执行效率上是最优的。程序只查询预期中的的数据,并不知道你要访问它的模型关系,当你需要访问模型关系时,再去查询一次就好了。 **但是我们需要注意的是**,当查询结果不是一个单条记录 (`Model`),而是多条记录 (`Collection`) 时,如果这个时候要去访问`Collection`中每条记录的模型关系,那就需要使用接下来的`预加载`了。否则就会产生上文的`N+1`的问题。 * * * 还是刚才的查询,这次使用`with()`预加载工人关系。 ~~~php public function index() { $factories = Factory::query()->with('workers')->get(); // 工厂表有 10 条记录 foreach($factories as $factory) { $factory->workers; } } ~~~ 此时 SQL 查询就只有 2 条。 ~~~php select * from `factories` select * from `workers` where `workers`.`factory_id` in (1, 2, 3, 4, 5, 6, 7, 8, 9, 10) ~~~ 还是上面的工厂例子,再举一次: ~~~php 工厂接到一笔新订单,是个急活,厂长周一就通知下去本周六加班,所有工人必须守候在流水线随时待命,这就叫`预加载`。 ~~~ 工人信息已经准备好了,只等着你去访问它就可以了。 * * * #### 由此可以得出结论: 当你的查询结果返回的是一个 单条记录 (`Model`),此时`懒加载`和`预加载`其实没有区别,因为每一个模型关系就是一次查询,所以这里不论是使用`with()`预加载,还是直接使用`懒加载`,对于单条记录的模型来说,最终 SQL 执行条数都是一样的。 但当你的查询结果返回的是多条记录 (`Collection`) 时,如果要访问模型关系,就**必须**使用`with()`预加载,否则就会产生`N+1`问题。 * * * #### 那么`load()`是干嘛的? 我们这样查询可以吗? ~~~php public function index() { $factory = Factory::query()->load('workers')->find(1); } ~~~ 结果是肯定不行的 ~~~php BadMethodCallException: Call to undefined method Illuminate\Database\Eloquent\Builder::load() ~~~ 在一个查询没有使用`get()`或`find()`返回之前,它都是一个`EloquentBuilder`对象,我们的所有`where()`、`with()`、`whereIn()`、等方法都是在构造查询语句,但其实并没有数据被真正的查询。 当这条语句被执行时,并没有 SQL 语句被执行。 ~~~php Factory::query()->with('workers'); ~~~ `load()`是模型`Model`才能使用的方法,`EloquentBuilder`是不能使用的。 * * * #### 看一下如何使用`load()`。 ~~~php public function index() { $factory = Factory::query()->find(1); $factory->load('workers'); } ~~~ 此时被执行的 SQL 语句是: ~~~php select * from `factories` where `factories`.`id` = '1' limit 1 select * from `workers` where `workers`.`factory_id` in (1) ~~~ 其实上面的查询和下面的这句`with()`是一模一样的: ~~~php public function index() { $factory = Factory::query()->with('workers')->find(1); } ~~~ #### 那么`load()`的使用场景是? 假如我们使用依赖注入的方式来查询`Factory`,但同时我们还要把`workers`关联一并返回的时候,就会用到它了。 ~~~php public function index(Factory $factory) { return $factory->load('workers'); } ~~~ `with()`是查询时一并加载模型关联,`load()`是先有模型被查询后,再加载模型的关联时使用的。