一次PostgreSQL复杂jsonb数据矫正过程分享

背景介绍

想看干货直接看最后的总结,其他流水账可以不看,也可以当故事看。

7月底我司某产品因故需要拉齐现场版本,其中某地版本较低,且曾经做过一些定制内容,升级前也未识别该情况,导致后续持续一个月不断地暴露新问题。因为涉及研发团队更换,交接不充分,代码管理不规范等原因导致的部分定制需求代码升级后丢失的问题暂且不表,今天就来说一下因模板识别错误,导致100多条业务数据的核心json数据混乱,最终导致客户无法正常基于json要素生成word的解决方案,主要是想传递一下json矫正场景的一个排查过程,给自己做个记录供下次遇到类似问题快速学习,另外也给其他有类似诉求的人一个参考思路。

过程

以下服务之间的关系存在精简,不要太在意,主要看思路。

问题

有这么A、B、C、D四个服务

  • A服务:互联网部署,可以让用户导入基于指定模板填充的excel内容,然后把excel的内容经过各种关系的组装,最终形成一个json(还有很多其他固定的业务字段有内容,本文主要讲解针对该字段的处理),存入数据库的业务表中,会有摆渡机制将同构的数据摆渡到内网的B服务中。
  • B服务:A服务在内网的镜像,数据与A服务基本保持一致。
  • C服务:基于B服务的一个基础应用,可将B服务中的json元素进行业务加工,并提供给业务层应用使用。
  • D服务:用户可见的应用,可以利用C服务生成指定格式的word文档。

同时A服务中又针对不同业务场景,提供了多套不同的excel模板,互联网用户根据需要使用对应模板填充不同业务数据,内网应用会根据模板类型标记来动态解析json数据,并逐层传递至用户层应用。

此次升级后,A服务中引入了一个缺陷,所有的excel模板,均会按照相同的模板类型A来解析,导致用户一直在用的模板类型B核心数据丢失,进一步导致用户层应用无法正常使用。

虽然我们及时(好像耗时4天找到了问题,是因为新研发改错了代码。毕竟新团队也不熟悉代码,逻辑也比较复杂,所以综合分析时间也还能接受)修复了该缺陷,但这几天已经产生了100多条有效数据(还有几十条我们自己的测试数据),如何矫正成了难题。

解决思路1(让用户搞)

因数据链路存在状态,所以研发提出直接驳回数据,让用户在互联网重新操作一遍,或者系统也提供了逐个记录编辑的功能,让用户把解析错误的数据重新手工填写一下是否可行?

这俩想法都不可行,手工填写的话,100多条数据,每条数据几十个信息项,甚至每条数据里面可能还有几十条子业务数据,让用户手工逐个矫正?我准备跟用户说这个解决方法之前,我都得扇自己两巴掌,最好带着掌印才好面对面跟用户说这个方案,否则我怕他们打我。

退回的话,虽然也给用户增加了不必要的工作量,但听起来还行,只是问题数据已经产生了将近一个月了,数据链路流转到哪儿了?随机抽查了几个,都已经审核了,没法修改了,所以让用户来搞的思路不可行了。

解决思路2(我们来搞)

既然有问题的只是json数据,那我们自己再重新导入一下(还好我机智,感觉到最后可能需要矫正,发现问题不久就找多个用户要来了他们的excel),然后根据规则,把新旧数据做个唯一映射,最后把解析正确的json更新到原来错误的字段里面不就可以了?

说干就干,我登录互联网系统把当时留存的excel导入系统,但不提交(这样就不会摆渡到内网,用完即可删除),然后仅把json数据相关的表数据筛出来,生成insert语句,导入内网(毕竟最终影响用户的是内网)。

然后,到这一步就卡住了,研发说映射条件得想想,想了一段时间,告诉我json中有一个元素存储的是人员名字数组,取第一个应该能对得上,但脚本怎么写呢?上网查吧。

假设:新导入的表为tb1_newdata,原业务表为tb1,json字段名为j_json,正确的json字段值为下列内容(实际内容复杂很多)。
附加条件:每次只允许申报一种非固定支出的耗材种类,且这次出问题的记录里面没有重名的情况(重名的会复杂一些,目前想着是把能匹配上的匹配上,重名的大不了手工找映射关系,增加虚拟字段存映射ID都行)

{
    "学校编码": "京0156",
    "规模": "大型",
    "市/区": "海淀",
    "法人": "曾书书",
    "班级": {
        "班级数": 50,
        "班级列表": [
            {
                "编号": "一零一",
            	"名称": "一年级一班"
            }
        ]
    },
    "年度预算": {
        "预算年度": 2023,
        "预算申报日期": "2023-09-30",
        "申报负责人": [
            {
                "随机ID": "011234",
                "姓名": "曾书书"
            }
        ],
        "申报组成员": [
            {
                "随机ID": "011234",
                "姓名": "曾书书"
            }, 
            {
                "随机ID": "011214",
                "姓名": "李老师"
            },
            {
                "随机ID": "015234",
                "姓名": "王老师"
            }
        ],
        "固定支出": {
            "薪资": 1000,
            "授权费": 5000
        },
        "非固定支出": {
            "本次申报类型": "实验耗材",
            "申报日期": "2023-08-26",
            "耗材": [
                {
                    "办公用品": {
                        "电子设备更新": 0,
                        "上年压款结算": 0,
                        "汇总日期": "",
                        "汇总人": ""
                    }
                },
                {                
	                "体育用品": {
                        "教学设备更新": 0,
                        "内训": 0,
                        "外训": 0,
                        "汇总日期": "",
                        "汇总人": ""
                    }
                },
                {
	                "实验耗材": { // 历史数据这里整组为空,当成“办公用品来解析了”
                        "上年预算": 6000,
                        "上年决算": 6300,
                        "本年预算": 6500,
                        "汇总日期": "2023-08-24",
                        "汇总人": "张三",
                        "大额清单": [
                            {
                                "序号": 1,
                                "商品A": 1000,
                                "负责人": "李四"
                            },
                            {
                                "序号": 2,
                                "商品B": 1500,
                                "负责人": "王五"
                            },
                            {
                                "序号": 3,
                                "商品A": 800,
                                "负责人": "马六"
                            },
                        ]
                    }
                }
            ]
        }
    }
}
  • 先备份原表数据(很重要哦),有问题可以随时恢复

    create table tb1_bak_20230826 as select * from tb1 where dt_createtime between 'xxx' and 'xxx'; -- 这里其实推荐备份全表,实验表再条件备份,因为我对自己有信心,所以这里就偷懒了
    
  • 再备份一份原表数据(用于试验更新结果,随时会重建)

    create table tb1_old as select * from tb1_bak_20230826;
    
  • 初步书写映射关系

    -- 1、先确认是否可以正确查询结果
    select j_json->'年度预算'->'固定支出' from tb1_old limit 1; -- 正确输出“薪资”、“授权费”等
    -- 2、确认数组内容是否可正确查询
    select j_json->'年度预算'->'非固定支出'->0 from tb1_old limit 1; -- 正确输出耗材整组内容
    -- 3、连表查询关联的结果总数
    select count(*) from tb1_newdata t1 inner join tb1_old t2 on t1.j_json->'年度预算'->'申报组成员'->0->'姓名' = t2.j_json->'年度预算'->'申报组成员'->0->'姓名'; -- 应该输出128,实际结果只有5条。为啥与预期差这么多?
    -- 4、根据姓名先检索对应ID
    select c_bh from tb1_newdata where j_json->'年度预算'->'申报组成员'::text like '%曾书书%'; -- 记录下来ID,并用同样的方式确认旧表里对应记录ID
    -- 5、分别更换表名使用脚本确认条件字段实际结果
    select j_json->'年度预算'->'申报组成员'->0->'姓名' from tb1_newdata where c_bh = '上面的结果'; -- 原因是数组实际内容没有变更,只是位置发生了变化,导致第0个元素不是同一个人了
    
  • 既然相等不起作用,那么我们用包含来判断?

    -- 网上查了下 @> 和 <@ 可以用于判断指定元素是否存在于指定数组中,那么开始试验
    -- 1、第一次尝试
    select * from tb1_newdata t1 inner join tb1_old t2 on t1.j_json->'年度预算'->'申报组成员' @> t2.j_json->'年度预算'->'申报组成员'->0; -- 执行直接报错,提示operator does not exists: boolean -> unknown
    -- 2、将随机某条结果结果直接拿出来检查,确认是否条件数据提取错误
    select j_json->'年度预算'->'申报组成员', j_json->'年度预算'->'申报组成员'->0 from tb1_newdata where c_bh = 'xxx'; -- 结果看起来没啥问题,前者是一个数组,后者是一组元素
    -- 3、脱离业务表,单独验证条件是否成立
    select '[{"随机ID":"011234","姓名":"曾书书"},{"随机ID":"011214","姓名":"李老师"},{"随机ID":"015234","姓名":"王老师"}]'::jsonb @> '{"随机ID":"011234","姓名":"曾书书"}'::jsonb; -- 结果是f,这为啥不匹配?奇哉!怪哉!脑瓜疼哉!
    -- 4、不知道怎么突然想起来,前者是数组,后者不是数组,试试加上[]?
    select '[{"随机ID":"011234","姓名":"曾书书"},{"随机ID":"011214","姓名":"李老师"},{"随机ID":"015234","姓名":"王老师"}]'::jsonb @> '[{"随机ID":"011234","姓名":"曾书书"}]'::jsonb; -- 嘿,您猜怎么着?那叫一个地道!不对,串场了不好意思。
    -- 结果竟然由f变成了t。奇哉!怪哉!脑瓜疼哉!
    -- 虽然但是,我也不想再前后拼接字符串来匹配,何况里面还有个随机的人员ID,两次导入的相同人也是变化的,此路不通
    
  • 那有返回数组指定元素位置的函数吗?

    很可惜,我没找到…(可能是我翻阅的资料还不够多,也可能是这种函数投入产出比不高,厂家没做)

解决思路3(还是我们来搞)

这…还能咋搞?总不能写个代码,逐个解析成结构化数据再匹配吧?这也太麻烦了(让人家用户手工重新导入的感觉估计会比我这时候的感觉更糟糕,本来准备excel就费劲,还有一堆其他事儿,你还让我因为非我导致的原因返工?)。

那就继续分析是否还有其他数据项可利用吧。

  • 全局搜索“曾书书”

    还是只搜到这一组。难道就没有其他地方有这个标记了?再一看,哦,记事本没有循环查找功能,光标就在这一组这里

  • 光标挪到最上面,重新查找

    盲生他发现了华点,原来还有一组“申报负责人”可以用,里面的这个“张老师”也是可以唯一标记问题记录的(实际业务比举例复杂,确实可以当做唯一标记,举例并没有那么严谨)

  • 脚本验证

-- 1、数据匹配(这就简单多了)
select count(*) from tb1_newdata t1 inner join tb1_old t2 on t1.j_json->'年度预算'->'申报负责人'->'姓名' @> t2.j_json->'年度预算'->'申报负责人'->'姓名'; -- 结果为128,数据量对上了。
-- 脚本的一小步,我处理问题的一大步,剩下的就是替换了,谷歌了一下,找到一个函数jsonb_set
-- 2、确认将要更新的内容是否正确
select jsonb_set(t2.j_json, '年度预算,非固定支出,耗材,实验耗材', t1.j_json->'年度预算'->'非固定支出'->'耗材'->'实验耗材'), t1.j_json->'年度预算'->'非固定支出'->'耗材'->'实验耗材', t2.j_json->'年度预算'->'非固定支出'->'耗材'->'实验耗材' from tb1_newdata t1 inner join tb1_old t2 on t1.j_json->'年度预算'->'申报负责人'->'姓名' @> t2.j_json->'年度预算'->'申报负责人'->'姓名'; -- 比对结果,一切正常
-- 3、书写更新脚本
update tb1_old set j_json = tmp.j_json from (
select jsonb_set(t2.j_json, '年度预算,非固定支出,耗材,实验耗材', t1.j_json->'年度预算'->'非固定支出'->'耗材'->'实验耗材'), t2.c_bh from tb1_newdata t1 inner join tb1_old t2 on t1.j_json->'年度预算'->'申报负责人'->'姓名' @> t2.j_json->'年度预算'->'申报负责人'->'姓名'
) tmp on tb1_old.c_bh = tmp.c_bh; -- 受影响行数128行
-- 4、随机抽查,验证结果(为了更直观及快捷的比对,又搜了美化json结果的函数,jsonb_pretty)
select jsonb_pretty(j_json) from tb1_old where c_bh = 'xxx'; -- 结果正确!哇的一声就哭了

但截至目前只是成功矫正了B服务,C服务需要删掉错误数据,重新再怎么触发一下就会自动同步最新数据,这128条数据有可能需要我手工逐个操作一遍,又会是一个大工程。研发已经下班啦,我CC也没搜到当时怎么重新同步的,只能再等研发支持啦,写个笔记就可以回去睡觉啦。

总结

这次我学到了什么呢:

  • ->加数字代表取数组的第n个元素;->加文本代表取json的指定对象(结果也是json格式);->>与->的用法一致,只是前者结果是文本,后者依然是json对象

  • jsonb_pretty函数可以美化json结果(注意结果不要直接放到记事本里面,放写字板或其他文本编辑工具,记事本展示结果

  • jsonb_set函数可以置换json中的指定元素

  • @> 操作符是判断左侧元素数组中是否包含右侧元素, >@ 操作符是判断右侧元素数组中是否包含左侧元素。左右两侧均可放多个元素比较,且不区分位置,全部包含即为真,否则为假(但需要判断的那个元素,貌似必须是以数组的形式书写,否则结果永远为假)

  • 最后说两句

    最后:团队交接,一定不要太匆忙,给一些原团队盘点以及新团队学习的时间呀!而且,新团队刚接手时,可得保障产出质量,不然整体成本增加不说,客户满意度还会下降,还会打击现场人员的升级积极性,得不偿失呀!

    最后的最后:其实这篇文章干货不多,例如没找到新的映射关系的话,当前的知识储备针对当前问题依然是无解的,最终可能还得写程序搞定,但运气好找到了唯一映射,运气也是实力的一部分,都要加油哦!

官方手册:http://postgres.cn/docs/11/functions-json.html

你可能感兴趣的:(postgresql,数据库,json)