ThinkPHP6 ORM 更新 JSON 字段优化

issue: 建议db json增加多个对象更新

1
2
3
4
5
6
7
8
9
public function index ()
{
Banner::where('id', 1)
->update([
'info->name' => 'hongfs',
]);

return json([]);
}

单条是可以正常执行的。

1
[SQL] UPDATE `fa_banner`  SET `info` = json_set(ifnull(`info`, '{}'), '$.name', 'hongfs')  WHERE  `id` = 1 [ RunTime:0.036457s ]
1
2
3
4
5
6
7
8
9
10
public function index ()
{
Banner::where('id', 1)
->update([
'info->name' => 'hongfs',
'info->hi' => 'hongfs',
]);

return json([]);
}

两条记录我们就收获了一个报错。

1
UPDATE `fa_banner` SET `info` = json_set(ifnull(`info`, '{}'), '$.hi', 'hongfs') WHERE `id` = 1

可以看到字段就只剩下最后一个了。

目前优化的思路还是先支持多个,通过全局搜索 json_set( 发现只存在于 vendor/topthink/think-orm/src/db/Builder.php

通过查阅 Mysql JSON_SET 文档,是可以一次性更新多个值的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
protected function parseData(Query $query, array $data = [], array $fields = [], array $bind = []): array
{
foreach ($data as $key => $val) {
if (false !== strpos($key, '->')) {
[$key, $name] = explode('->', $key, 2);
$item = $this->parseKey($query, $key);

if(isset($result[$item]) && strpos($result[$item], 'json_set(') === 0) {
// 如果存在值且是 JSON 赋值格式那就是追加模式
$result[$item] = substr($result[$item], 0, -1) . ', \'$.' . $name . '\', ' . $this->parseDataBind($query, $key . '->' . $name, $val, $bind) . ')';
} else {
// ifnull 用于兼容字段为 `null`
$result[$item] = 'json_set(ifnull(' . $item . ', \'{}\'), \'$.' . $name . '\', ' . $this->parseDataBind($query, $key . '->' . $name, $val, $bind) . ')';
}
}
}
}

可以看见现在 SQL 语句是正常的了,程序也没报错了。

1
UPDATE `fa_banner`  SET `info` = json_set(ifnull(`info`, '{}'), '$.name', 'hongfs', '$.hi', 'hongfs')  WHERE  `id` = 1

还有另外一个场景,新增/更新和覆盖同时存在。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public function index ()
{
Banner::where('id', 1)
->json(['info'])
->update([
'info->name' => 'hongfs',
'info->hi' => 'hongfs',
'info' => [
'hongfs' => 'hongfs',
],
]);

return json([]);
}

如果你执行,现在还是会报那个错误。

如果不预先使用 json() 把字段添加进去,那会收到一个 “未定义数组下标: 0” 的错误。

这时我看了下报错的语句,发现只剩下后半段了。

通过使用 Xdebug 发现被截断的位置是 vendor/topthink/think-orm/src/db/PDOConnection.php:getRealSql 函数。

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
/**
* 根据参数绑定组装最终的SQL语句 便于调试
* @access public
* @param string $sql 带参数绑定的sql语句
* @param array $bind 参数绑定列表
* @return string
*/
public function getRealSql(string $sql, array $bind = []): string
{
foreach ($bind as $key => $val) {
$value = strval(is_array($val) ? $val[0] : $val);
$type = is_array($val) ? $val[1] : PDO::PARAM_STR;

if (self::PARAM_FLOAT == $type || PDO::PARAM_STR == $type) {
$value = '\'' . addslashes($value) . '\'';
} elseif (PDO::PARAM_INT == $type && '' === $value) {
$value = '0';
}

// 判断占位符
$sql = is_numeric($key) ?
substr_replace($sql, $value, strpos($sql, '?'), 1) :
substr_replace($sql, $value, strpos($sql, ':' . $key), strlen(':' . $key));
}

return rtrim($sql);
}

循环绑定参数然后进行真实数据赋值,但是我们会存在覆盖行为,一些参数再也用不到,但这里还是进行循环的。所以我们可以在最前面加个判断,如果参数标识值不存在则可以跳过。

1
2
3
4
5
6
7
8
public function getRealSql(string $sql, array $bind = []): string
{
foreach ($bind as $key => $val) {
if(strpos($sql, ':' . $key) === false) {
continue;
}
}
}

执行后 SQL 是正常的,但还是之前的那个错误。

我注意到了具体的报错信息,通过调用链路的信息结合报错内容,我开始猜测是不是绑定参数有问题。我又回到了 parseData,如果被覆盖了,那应该清除原本这个字段的绑定参数。

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
protected function parseData(Query $query, array $data = [], array $fields = [], array $bind = []): array
{
foreach ($data as $key => $val) {
if() {
} elseif (is_scalar($val)) {
// 过滤非标量数据

// 清除 JSON 字段已绑定的变量
// Raw 我不太熟悉,这里就暂不兼容了
if(isset($result[$item]) && !$data instanceof Raw && in_array($key, $options['json'])) {
$binds = array_filter($query->getBind(false), function ($bind_key) use($result, $item) {
return strpos($result[$item], ':' . $bind_key) === false;
}, ARRAY_FILTER_USE_KEY);

$query->getBind();

foreach ($binds as $bind_key => $bind_value) {
$query->bindValue($bind_value[0], $bind_value[1], $bind_key);
}
}

$result[$item] = $this->parseDataBind($query, $key, $val, $bind);
}
}
}

这时我们的语句就正常了。

1
UPDATE `fa_banner`  SET `info` = '{\"hongfs\":\"hongfs\"}'  WHERE  `id` = 1

到这里会发现其实 getRealSql 那一步就没必要了的。

最后,如果是这种呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public function index ()
{
Banner::where('id', 1)
->json(['info'])
->update([
'info' => [
'hongfs' => 'hongfs',
],
'info->name' => 'hongfs',
'info->hi' => 'hongfs',
]);

return json([]);
}
往上