SQLite json_patch 的数组边界:只想加一个标签,为什么旧列表整个被换掉
配置页面允许用户修改颜色、补一个标签、清空备注。后端把这一小段JSON交给json_patch,颜色更新正确,原来的两个标签却只剩下新标签,备注字段更是彻底消失。函数名称里的patch容易让人联想到逐项补丁,但它采用的是JSON Merge Patch规则,数组和对象的合并方式并不相同。
本例在Linux、CPython 3.12.14与SQLite 3.53.1实跑。SQLite需要提供JSON函数;标准构建从3.38.0起默认包含它们。保存程序为demo.py,运行python3 demo.py。数据仅放在内存连接中,参数使用占位符传入,不拼接成SQL语句,也不会改动磁盘数据库。
AI生成的概念示意图:整只小托盘的更换表示数组作为一个整体处理,淡色圆片表示移除字段;不是数据库结构图。
完整程序与本次实跑
完整可运行程序
import json
import sqlite3
base = {"theme": {"color": "blue", "size": 2},
"tags": ["draft", "local"], "note": "keep"}
patch = {"theme": {"color": "green"}, "tags": ["ready"], "note": None}
con = sqlite3.connect(":memory:")
try:
def merge(target, change):
text, = con.execute("SELECT json_patch(?, ?)",
(json.dumps(target), json.dumps(change))).fetchone()
return json.loads(text)
result = merge(base, patch)
assert result == {"theme": {"color": "green", "size": 2}, "tags": ["ready"]}
print("merged:", json.dumps(result, sort_keys=True))
print("original tags:", base["tags"])
print("null deletes note:", "note" not in result)
replacement = merge(base, ["only"])
assert replacement == ["only"]
print("root array patch:", replacement)
stored_null, = con.execute("SELECT json_set(?, '$.note', NULL)",
(json.dumps(base),)).fetchone()
assert json.loads(stored_null)["note"] is None
print("json_set keeps null field:", "note" in json.loads(stored_null))
finally:
con.close()本次实际输出(以下为结果,不是程序)
merged: {"tags": ["ready"], "theme": {"color": "green", "size": 2}}
original tags: ['draft', 'local']
null deletes note: True
root array patch: ['only']
json_set keeps null field: True先检查对象里什么被保留
merged里theme仍有size,color则变成green。补丁中theme的值仍是对象,因此合并会进入这个对象,按成员继续处理。没有出现在补丁里的size不会凭空消失。这个结果适合局部修改具有稳定字段名的配置,例如显示主题、分页设置或通知选项。
程序把结果解码成Python对象后断言其结构,输出时排序键只为方便阅读。不要用输出文本的成员顺序判断合并是否正确;JSON对象的字段顺序不是这里的业务条件。真正需要核对的是字段存在性、值的类型以及嵌套层级。
数组没有逐元素合并的默认身份
tags最终只有ready,原来的draft和local都没有保留。Merge Patch把整个数组当作一个值,新数组替换旧数组,不会把同一位置当作同一个对象,也不会按标签文字去重后追加。即使数组里是带id的对象,这个函数也不会自行把id识别为合并键。
如果需求确实是追加标签,应另行定义追加操作,或者读取并验证旧数组后构造目标数组,再在合适的事务条件下更新。直接把客户端提交的单个新标签包成数组,只是在请求“最终数组就是这一项”。更新协议要表达业务动作,不能依赖函数替调用方猜意图。
补丁里的null表示删除成员
null deletes note为True,因为对象补丁把note设为null时,成员会被移除。它不是保留note并将内容清空。后面的json_set对照保留了该字段,值为JSON null,说明“字段缺失”和“字段存在但为空”可以得到两种不同结构。
如果下游用字段缺失表示继承默认值、用null表示明确禁用,这个区别会改变业务结果。需要保存显式null的接口,不适合直接把所有用户输入解释成Merge Patch;可以改用具有独立操作类型的协议,并分别测试删除与置空。
根值也能被整体替换
root array patch得到only组成的数组,原来的根对象完全被替换。这与嵌套数组的整体处理相呼应:当补丁本身不是对象时,结果就是补丁值。假如配置必须始终是对象,应用要在执行前检查根类型,执行后也要校验完整配置约束。
original tags仍打印原列表,原因是本例向SQL传入JSON文本并接回新结果,没有原地改写Python字典。若结果之后需要落库,应当保存返回值;仅调用SELECT而不处理结果,不会自动更新某条表记录。本文用只读表达式特意把计算结果与持久化分开。
把补丁验收写成结构问题
上线前至少准备四组输入:对象只改一个子字段、数组缩短、成员置null以及根类型改变。再补充不合法JSON和不满足业务模式的结果。数据库接受JSON语法,只证明输入能被解析,不证明配置能被应用正常使用。
多客户端编辑同一份配置时,还要另行处理版本冲突。这个示例只解释一次确定输入的合并规则,没有提供并发控制或历史恢复功能。先把数组与null的合同写清,再讨论保存流程,才能避免一次看似很小的修改删除用户原有配置。
参考资料
官方资料核验于2026年10月2日;上述输出来自本次固定输入实跑,退出码为0。


