之前有一段代码使用了Laravel模型关联的预加载with()
方法,代码类似如下
|
|
结果类似如下
|
|
造成了某些结果的servant属性对应的是空,但事实上他们在数据库中是有对应关联数据的,然后造成了一个bug. 今天重新看了下 with()
的源码感觉终于知道了原因
创建测试数据
为了复现上面的问题 现在建2个表来模拟下,首先创建 master 当作主表,然后创建 servant 作为子表,他们之间的对应关系是 一对多 即一个 master 有多个 servant。另外我没有建立主外键关系,大概知道就好了!
|
|
正常的对应关系如下 纪晓岚有3个从者 和珅有2个从者
|
|
下面来看源码,一步步找到出现问题的原因
一对多关联
Laravel中实现一对多关联关系可以通过使用 hasMany()
方法,所以在Master的Model中我加入了
|
|
看下源码中 在 \vendor\laravel\framework\src\Illuminate\Database\Eloquent\Concerns\HasRelationships.php
文件中
|
|
其创建过程就是一些设置 父模型、子模型、关联字段、关联约束等
预加载 with()
方法
Laravel提供了预加载的功能 这样可以避免出现重复查询 并且可以把查询降低到2次
看下 with()
方法的源码, 在文件 \vendor\laravel\framework\src\Illuminate\Database\Eloquent\Model.php
中
|
|
然后看下 Builder 下的 with()
方法 在文件 \vendor\laravel\framework\src\Illuminate\Database\Eloquent\Builder.php
中
|
|
下面看下 parseWithRelations()
方法 还是和上面同一个文件中
|
|
所以在执行完 Master::with('servant')
后 在Builder的with() 方法中设置了一个 eagerLoad
属性 其值为
|
|
然后返回了该 Builder 对象
get()
方法
在看下最后执行的 get()
方法 还是在上面的同一个文件中
|
|
下面看 eagerLoadRelations()
方法 还是同一个文件
|
|
下面看 eagerLoadRelation()
方法的实现 还是同一个文件
|
|
好了一通设置后最终返回了 \Illuminate\Database\Eloquent\Collection
对象数组 这个很熟悉了吧 走到这里每个master下都设置上了一个名为 servant 的属性其值对应的是 Collection(Servant)
,所以后续的操作比如直接取出关联子表中的属性其实都是从这里直接取出,并不会去查询数据库了
预加载SQL执行过程
讲完了底层实现,下面看下执行过程中到底都执行了什么SQL。为了方便看结果 直接使用 Laravel-debugbar 来监测
1.加载全部
|
|
2.给关联增加条件
|
|
其实增加条件过滤、筛选字段过滤、排序等都不影响。其最终只会执行2条SQL
3.给关联设置first()
|
|
这个时候他查询的都是同一条SQL。因为根据源码分析 他最后的关联是使用了 wherein(关联字段, [所有主表关联主键的数组])
这种形式来设置约束条件 所有执行的都是同一条SQL。(PS:上面为什么是in(1, 2) 因为我只有2条测试数据,关联主键分别是1 和 2 如果你有3条记录主键分别是 5 6 7 那他就是in(5, 6, 7)了)。
正常来说会在执行 get()
的时候发生2条SQL查询 一条是查询全部的 一条是根据全部的id查询匹配条件的,总共2条SQL
为什么会变成3条SQL呢,因为在 eagerLoadRelation()
中 $constraints($relation);
这里把同一个 Relation
传进了闭包 然后在外面执行 $query->select('id', 'master_id', 'name', 'level')->orderBy('level', 'ASC')->first();
会多一次查询出来 查询内容是一样的。
然后 first()
会使用 limit 1
来过滤结果 所以最终查询到的子结果只有一条,然后在给Master设置属性的时候会有一个关联条件的匹配,而限制了只有一条基本只会命中一个Master的ID了,所以造成了最终的结果中 有些有关联属性 有些却为空
解决办法
一种就是先给主表查询全部(或者按指定条件查询主表),然后在用到的子表数据的时候去动态属性的形式来加载出子表的数据
|
|
不过这样会造成一个问题就是每天子表数据取出来都会有一次查询产生
第二种办法就是查询的时候只按需要条件过滤 然后在取数据的时候自己去获取需要的数据 如下
|
|