通过这次联合(union)查询了解 Laravel 预处理的数据绑定
通过这次联合(union)查询了解 Laravel 预处理的数据绑定
最近做一个需求,业务要求把两种不同的记录放在同一个列表中进去筛选查询,需要查询两种记录关联的客户信息。虽然两种记录字段差距很大,但是好在我们需要的字段不多,用联合(union)查询也能做。
开始实验
这里先描述本次实验的大致情况
- version: Laravel 5.5
- Model:
customers
->trial_records
一对多customers
->onetoone_records
一对多- 需求:
- 将
trial_records
和onetoone_records
联合起来并关联customers
,通过记录的状态,记录的开始时间和客户的创建时间来筛选查询结果
第一次尝试
为了方便对比,这次的查询条件都是写死了
联合查询
查询上课时间在 4 月份的,记录状态为成功(2)的
1 | $classStartAt = Carbon::parse('2019-04-01 10:07:57')->startOfMonth(); |
关联客户表
将联合查询结果起个别名,通过 customer_id 关联
1 | $query = DB::table(DB::raw('(' . $queryUnion->toSql() . ') as data')) |
最后的条件
对客户的创建时间筛选
1 | $startAt = Carbon::parse('2019-03-01 10:07:57')->startOfMonth(); |
执行程序
Postman 测试
执行结果
1 | { |
SQLSTATE[HY093]: Invalid parameter number (SQL: select
customers
.name
,customers
.id
ascustomer_id
,customers
.created_at
,data
.status
,data
.start_at
,data
.type
,data
.id
from ((selectid
,customer_id
,status
, @type := 2 as type,start_at
fromtrial_records
wherestart_at
between 2019-03-01 00:00:00 and 2019-03-31 23:59:59 andstatus
= ?) union (selectid
,customer_id
,status
, @type := 1 as type,start_time
asstart_at
fromonetoone_records
wherestart_time
between ? and ? andstatus
= ?)) as data inner joincustomers
oncustomers
.id
=data
.customer_id
andcustomers
.deleted_at
is null wherecustomers
.created_at
between ? and ?)
报错了,看到爆出错误的 sql 中有很多问号,除了第一个start_at
有查询时间,剩下的都是问号。
分析
为什么呢?我们都知道问号是预处理的占位符,这显然是数据没有绑定上。
回看代码,关联客户表的时候,我们将 $queryUnion
的 sql 拼接了,但是原属于 $queryUnion
的数据绑定并没有绑定到 $query
上。
OK,找到了原因,我们继续。
第二次尝试
修改关联客户表这一段,继续查询
修改代码
mergeBindings
1 | $query = DB::table(DB::raw('(' . $queryUnion->toSql() . ') as data')) |
执行代码
1 | { |
这次没报错,但是空数据了。
查看日志
日志中的 sql 长这样
1 | select `customers`.`name`, |
这样的 sql 能查询出来数据才怪了,我们可以看到数据的绑定一团乱麻了已经。
为什么会这样呢?
源码分析
我们查看
Illuminate\Database\Query\Builder
get()
runSelect 就是查询
1 | public function get($columns = ['*']) |
runSelect()
看到了
getBindings()
1 | protected function runSelect() |
getBindings()
属性
bindings
被转成一维数组返回
1 | public function getBindings() |
Arr::flatten() 是一个多维数组转一位数组的方法
1 | // Illuminate\Support\Arr |
看来问题的关键是 bindings
我们继续分析这个属性
bindings
bindings
是一个二维数组,注意是公开属性
1 | public $bindings = [ |
看到这里我已经大概明白为什么bindings
这么设计并不难理解, laravel
会将所有绑定的数据分好类,这样即使你的query
没有按照 select > join > where > having > order > union
这样的顺序来写,查询的时候也一样能够对应准确的占位符号。
在我们这个例子中为什么就发生了偏差呢?
是 mergeBindings
有什么问题吗?
mergeBindings()
这就是递归合并了两个不同
query
的bingdings
,注意这是一个公开的方法
1 | public function mergeBindings(self $query) |
打印 bindings
修改代码打印
bindings
1 | \Log::info($queryUnion->bindings); |
查看结果
$queryUnion->bindings
1 | { |
$query->bindings
1 | { |
看到这里,其实已经很清楚了。mergeBindings()
只是对两个 query
的 bindings
的简单递归合并。
在我这里的情况并不适用,因为我这里是把 $queryUnion
的结果集作为一个表进行了内链接查询,在 $queryUnion
中的 bindings
应该全部放到 $query->bindings['join']
中才对。
明白了原因就能找到应对的方案了。
第三次尝试
修改关联查询代码
将
$queryUnion->bindings
转成一维数组,放进$queryUnion->bindings['join']
,然后在合并
1 | $queryUnion->bindings['join'] = Arr::flatten($queryUnion->bindings); |
执行
很好,是我想要的结果
1 | { |
相应的 sql
1 | select `customers`.`name`, |
总结
这个问题的解决办法没有什么太好的能够统一解决的,只能说我们需要了解有这个问题,那么编写代码的时候应当注意,注意避坑。
在我这样的业务中,我选择了将第一个 query
中的 bingdings
全部放到 join
段,因为我的业务中,这些数据确实应该存在于第二个 query
的 join
段。
所以遇到这样的问题,大家还是应该灵活去应对。
当然,如果你有其他的想法,欢迎表达!