BUG
2018年12月2号,用户反馈做了一天的图,保存后发现一天白干了,添加的内容没有保存上。
排查
查看用户的文件,解压缩后的数据库文件中,图层集中存在动力电缆(数据集同),但是图层表不存在。
重现
根据用户反馈的log,根据其2018年11月22日全天从早到晚的操作步骤,逐步模拟,得以重现。
1、打开一张dwg,执行modifysymbol,保存文件。
2、重新打开文件,再执行modifysymbol,添加电缆——失败!!!
3、可怕的是这个失败没有提示,用户以为成功了,继续作图。。。
分析
1、与dwg关系不大,重点在于必须是一张有多个地图的lfmx。
2、执行modifysymbol会在非当前地图中执行写入操作(添加对象)。
总结
1、直接原因:动态加载图层开启后,如果图层处于隐藏或者不属于当前打开的地图,则此图层不会在打开数据源时加载。
2、根本原因:sqlite记录集不合理的访问方式。
细化
在描述错误之前,先了解一下SQLITE的创建表格和查询冲突。
A、在一个事务中,若创建了表格之后试图进行查询,报错(错误码6)。
B、若创建表格提交,再下一个事务查询,则正常。
C、如果一个查询没有完成(游标没有走到尽头),就创建表格,会报错(错误码6),无论当前事务还是下一个事务。
本次的冲突属于冲突C,当用户打开dwg转换过来的数据源,有两张地图,默认显示第一张地图,其中的图层被加载。而第二张地图中的图层没有加载(见上总结)。当执行修改符号命令时,直接操作没有加载的数据集(删除实体、添加实体),然后保存。在保存时,检测到数据集没有加载,于是加了这一句:
1 | _pDRS = new DataRecordSet(_pSdeDataSet->query( _T("1>0" ), _T("LRID,LRENTITYDATA")), pDataSource, _guid); |
为了简化理解,这句代码相当于:
1 | _rs = _pSdeDataSet->query( _T("1>0" ), _T("LRID,LRENTITYDATA")); |
这里的_rs是数据集一个成员,这句话的任务在于创建了一个记录集,以便支持后面的增删改操作。为何这一句简单代码就造成了错误呢???
因为在记录集构造函数中,有一个非常隐秘的操作,默认生成了一个游标!!!相当于在构造函数中执行了moveFirst。所以当前常见的记录集访问代码经常如下:
1 | IRecordSet* ir = _pSdeDataSet->query( _T("1>0" ), _T("LRID,LRBOXLEFT,LRBOXRIGHT,LRBOXBOTTOM,LRBOXTOP,LRENTITYDATA")); |
这种方式,看似简单,省略了moveFirst,实则极不合理。由于这个构造函数默认游标的生成,导致接下来创建表格都将失败!!!原有的加载数据不会出错,皆是因为都通过moveNext将游标使用完毕,使其指向了结尾。
修改
这次错误原因找到,但是检测出要改的地方很多:
1、记录集这种隐形的游标操作非常危险,需要废弃或者改变数据访问方式。
2、数据集或者图层数据添加应该原子化,不能图层表格创建失败,地图中却添加成功。
3、直接操作未加载的图层(或者数据集),抛出异常或者提示错误或者补充加载,否则这些操作都将被忽略。
4、打开文件时,若检测到图层表或者数据集表不存在,需要提示或者补表。
首先,记录集构造函数将不再执行moveFirst,所以在查询得到结果后会执行释放操作,这样以后的记录集在使用时必须先调用moveFirst,否则将报错:
1 | IRecordSet* ir = _pSdeDataSet->query( _T("1>0" ), _T("LRID,LRBOXLEFT,LRBOXRIGHT,LRBOXBOTTOM,LRBOXTOP,LRENTITYDATA")); |
经过改动之后,当再次出现:
1 | _rs = _pSdeDataSet->query( _T("1>0" ), _T("LRID,LRENTITYDATA")); |
这样的代码时,将不再会默认生成一个游标,也就不影响表格创建了。
但是,如果开发人员不小心写了下面的代码怎么办?
1 | _rs = _pSdeDataSet->query( _T("1>0" ), _T("LRID,LRENTITYDATA")); |
莫名其妙的执行了一个moveFirst,然后就撒手不管了。这样将再次导致事务无法进行表格创建了,所以在事务提交时,将检查所有的记录集,如果出现了moveFirst之后没有指向结尾,也没有释放的记录集,直接报错(提醒开发人员必须禁止这样的代码),并手动moveNext到结尾。
除了moveNext,也可以直接将其释放来防止游标影响表格创建:
1 | _rs = _pSdeDataSet->query( _T("1>0" ), _T("LRID,LRENTITYDATA")); |