ThinkPHP8 ORM 复杂的关联场景下查询语句表名生成异常

issue: 能否完善关联统计相关方法兼容主表有别名的情况

这个问题我很久之前就遇到过,那会最方便的解决方法是在查询条件里面加上 Model 的类名,比如 where 条件的 field=id,换成 field=Order.id 这样子,不过会影响后续接盘的人。

这里我们先复现一下这个问题,我使用了 https://raw.githubusercontent.com/hhorak/mysql-sample-db/mysql-5.7/mysqlsampledatabase.sql 作为我们的测试数据,下面我就直接贴上 Model 的代码了。

app/model/Order.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?php
declare (strict_types = 1);

namespace app\model;

use think\Model;

/**
* @mixin \think\Model
*/
class Order extends Model
{
protected $name = 'orders';

public function details()
{
return $this->hasMany(OrderDetails::class, 'orderNumber', 'orderNumber');
}
}

app/model/OrderDetails.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
declare (strict_types = 1);

namespace app\model;

use think\Model;

/**
* @mixin \think\Model
*/
class OrderDetails extends Model
{
protected $name = 'orderdetails';
}

好了,现在我们在控制器进行查询来复现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<?php

namespace app\controller;

use app\BaseController;
use app\model\Order;
use app\Request;

class Index extends BaseController
{
public function index(Request $request)
{
$data = Order::with([
'details'
])
->withSum('details', 'priceEach')
->hasWhere('details', function ($query) {
$query->where('quantityOrdered', '>', 40);
})
->where('orderNumber', '<', 10120)
->select();

dump($data->toArray());
}
}

复现到了。

1
2
3
SQLSTATE[42S22]: Column not found: 1054 Unknown column 'orders.orderNumber' in 'where clause'

SELECT *,(SELECT SUM(`priceEach`) AS think_sum FROM `orderdetails` `sum_table` WHERE ( `sum_table`.`orderNumber` =orders.orderNumber )) AS `details_sum`,`Order`.* FROM `orders` `Order` JOIN `orderdetails` `OrderDetails` ON `Order`.`orderNumber`=`OrderDetails`.`orderNumber` WHERE `OrderDetails`.`quantityOrdered` > '40' AND `orderNumber` < 10120 GROUP BY `Order`.`orderNumber`

现在我们开始处理这个问题,因为之前有相关处理经验,所以我们直接去看 vendor/topthink/think-orm/src/db/builder/Mysql.php 文件,涉及语句的生成都在这里,我们不考虑非 MySQL 的情况。

注意 select() 这个方法,这里生成的就是查询的代码,我们来段输出看看是不是。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public function select(Query $query, bool $one = false): string
{
$options = $query->getOptions();

$value = str_replace(
['%TABLE%', '%PARTITION%', '%DISTINCT%', '%EXTRA%', '%FIELD%', '%JOIN%', '%WHERE%', '%GROUP%', '%HAVING%', '%ORDER%', '%LIMIT%', '%UNION%', '%LOCK%', '%COMMENT%', '%FORCE%'],
[
$this->parseTable($query, $options['table']),
$this->parsePartition($query, $options['partition']),
$this->parseDistinct($query, $options['distinct']),
$this->parseExtra($query, $options['extra']),
$this->parseField($query, $options['field'] ?? []),
$this->parseJoin($query, $options['join']),
$this->parseWhere($query, $options['where']),
$this->parseGroup($query, $options['group']),
$this->parseHaving($query, $options['having']),
$this->parseOrder($query, $options['order']),
$this->parseLimit($query, $one ? '1' : $options['limit']),
$this->parseUnion($query, $options['union']),
$this->parseLock($query, $options['lock']),
$this->parseComment($query, $options['comment']),
$this->parseForce($query, $options['force']),
],
$this->selectSql
);

dump('SQL 生成: ' . $value);

return $value;
}

好了,确定是这里,命名已经非常清楚告诉你每一个函数的作用了,所以我们直接去看 parseWhere 这个函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
protected function parseWhere(Query $query, array $where): string
{
$options = $query->getOptions();
$whereStr = $this->buildWhere($query, $where);

dump($whereStr);

if (!empty($options['soft_delete'])) {
// 附加软删除条件
[$field, $condition] = $options['soft_delete'];

$binds = $query->getFieldsBindType();
$whereStr = $whereStr ? '( ' . $whereStr . ' ) AND ' : '';
$whereStr = $whereStr . $this->parseWhereItem($query, $field, $condition, $binds);

dump('处理后:' . $whereStr);
}

return empty($whereStr) ? '' : ' WHERE ' . $whereStr;
}

进来后打印些日志,发现处理都来自 parseWhereItem,我们在 parseWhereItem 里面打印些日志看看。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
protected function parseWhereItem(Query $query, $field, array $val, array $binds = []): string
{
// 字段分析
$key = $field ? $this->parseKey($query, $field, true) : '';

[$exp, $value] = $val;

dump([
'name' => 'parseWhereItem',
'key' => $key,
'exp' => $exp,
'value' => $value,
]);
}

好了,你可以看见这里存在两个问题,第一个的 RAW 的 value 没有使用模型名称,最后一个的 key 也一样。

可能你会有疑问为什么两个都不是统一的问题,RAW 是用来处理字段和字段之间的判断问题,如果你直接使用 where 那会当普通字符串来处理。

我们先来处理下面这条语句,因为这条语句的生成在模型关联之前,所以这里我们在 parseWhereItem 里面是获取不到相关信息的。

1
SELECT SUM(`priceEach`) AS think_sum FROM `orderdetails` `sum_table` WHERE ( `sum_table`.`orderNumber` =orders.orderNumber )

我们需要跳转到 parseKey() 函数,上一次我们处理 JSON 也是在这里的,这次我们需要通过强制的方式来进行对修改,所以我没必要想过要提交 PR,这里的不确定会有一些。

我们在 parseKey() 的最后来输出下返回值,看一下我们要怎么识别出这段 SQL。

1
2
3
4
5
6
public function parseKey(Query $query, string|int|Raw $key, bool $strict = false): string
{
dump($key);

return $key;
}

嗯,我们这边选 AS think_sum FROM 作为判断条件吧,然后将 =orders. 作为替换的条件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public function parseKey(Query $query, string|int|Raw $key, bool $strict = false): string
{

if(str_contains($key, 'AS think_sum FROM')) {
$alias = $query->getOptions('alias');

if($alias) {
foreach ($alias as $alias_key => $alias_value) {
$key = str_replace(
[' =' . $alias_key . '.'],
[' = `' . $alias_value . '`.'],
$key,
);

break;
}
}
}

dump($key);

return $key;
}

可以看见语句变正常了,这里已经使用上了模型名称。

1
(SELECT SUM(`priceEach`) AS think_sum FROM `orderdetails` `sum_table` WHERE  ( `sum_table`.`orderNumber` = `Order`.orderNumber ))

现在我们回到 parseWhereItem,我们需要在这里处理最后一个问题,这里是基础的 where 条件没有带上模型名称,我们就直接判断有没有 . 来进行处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
protected function parseWhereItem(Query $query, $field, array $val, array $binds = []): string
{
if(!str_contains($key, '.')){
$alias = $query->getOptions('alias');

if($alias) {
foreach ($alias as $alias_value) {
$key = '`' . $alias_value . '`.' . $key;
break;
}
}
}

if ($value instanceof Raw) {
} elseif ($value instanceof Stringable) {
// 对象数据写入
$value = $value->__toString();
}
}

好了,我们刷新已经可以查询出来数据了(记得把之前的 dump 去掉)。

往上