pbai 发表于 前天 09:06

PowerBuilder DataWindow 更新机制全解:Update 的 SQL 生成规则、UpdateWhere 四态、并发冲突与 SetItemStatus 实测(PB10 基准 · PB12.5 实测)

PowerBuilder DataWindow 更新机制全解:Update 的 SQL 生成规则、UpdateWhere 四态、并发冲突与 SetItemStatus 实测(PB10 基准 · PB12.5 实测)


阅读说明
1. 适用版本:基准 PB10,兼容 PB 12.5(本文 13 个探针两版全部编译通过,PB12.5 逐个实机运行,差异之处单独标注)
2. 支持数据库:本文依赖数据库做真验证(SQL Server 2008 R2 实测,其他库行为请自行对拍)
3. 操作系统与环境要求:Windows 7 及以上;PB 10 或 PB 12.5 开发环境即可,无需第三方运行库
4. 难度系数:★★★★☆(难点在缓冲区状态与 WHERE 生成规则的组合理解,API 本身不难)
5. 其它阅读说明:本文讲「Update 到底执行了什么 SQL、冲突时会发生什么」;「保存前怎么校验、怎么撤销」在另一篇《DataWindow 撤销与保存前校验》里,两篇互补不重叠。示例用 DataStore 演示,结论同样适用于窗口里的 DataWindow 控件。


DataWindow 的 Update 是我们天天在调的函数,但很多人对它的认知停在「调用一下就存盘了」。它内部到底生成了什么 SQL?WHERE 条件怎么拼的?两个人同时改一行会怎样?主键没了还能不能更新?这些问题的答案,文档只给了一半,另一半得靠实机跑。我这次搭了 13 个探针,连真库逐个验证,把 SQLPreview 捕获到的原始 SQL 全部记下来,这篇就是实验记录的整理版。

一、实验环境与办法

先交代怎么测的,结论才可信:


[*]数据库:SQL Server 2008 R2,本地实例;测试表 t_up74(id int 主键,name nvarchar(10),qty int),两行种子数据 (1,'A',10)、(2,'B',20)
[*]探针:13 个独立 NVO(uo_u00 到 uo_u12),每个探针删表重建、用 SyntaxFromSQL 造一个 grid 风格的 DataStore、SetTransObject 后做一组操作
[*]捕获:自定义一个 datastore 子类 uo_ds74,在 sqlpreview 事件里把生成的 SQL 追加写到日志文件,操作完再读回来对照
[*]事务:SQLCA.AutoCommit = true(个别探针单独关掉测事务边界,见第七节)


13 个探针在 PB10 下全部编译 0 错误,PB12.5 下全部编译 0 错误并逐个运行通过。下面每条结论都标了探针号,可复现。

先给一个捕获 SQL 用的 datastore 子类,全文照抄就能用:


// uo_ds74.sru 的核心:sqlpreview 事件捕获 Update 生成的 SQL
// 文件须为 GBK 编码、CRLF 换行,直接 Import 到 PBL
event sqlpreview;
long ll_f
ll_f = FileOpen('up74_sql_log.txt', LineMode!, Write!, Shared!, Append!)
if ll_f >= 1 then
        FileWrite(ll_f, sqlsyntax)   // sqlsyntax 就是即将发给数据库的 SQL 原文
        FileClose(ll_f)
end if
end event


用法上只有一处要注意:事件参数叫 sqltype(用不着也可以不声明),SQL 原文在 sqlsyntax 里。把这个子类当成 datastore 用,Create 之后正常 SetTransObject、Retrieve、Update,日志里就有全部 SQL。

二、Update 生成的 SQL 长什么样(u00、u01)

基线探针 u00:造数、Retrieve、InsertRow 后给三列都赋值,然后 Update。捕获到的 SQL:


INSERT INTO dbo.t_up74 ( id, name, qty ) VALUES ( 3, 'C', 30 )


三个值得说的细节:


[*]列是全列:新增行默认 update=yes,三列都进了 INSERT。如果你给某列关掉 update(id.update=no),它就从 INSERT 里消失。
[*]没赋值的列不出现:u01 里我只给 id 和 name 赋了值,qty 留空,捕获到的 INSERT 就只有两列:INSERT INTO t_up74 ( id, name ) VALUES ( 3, 'C' )。PB 不会替你补 NULL,数据库侧该列是什么默认值就是什么默认值。
[*]状态在 Update 后被重置:新行插入前是 NewModified!,Update 成功并 COMMIT 后查 GetItemStatus,已经变成 NotModified!。这就是为什么再按一次保存按钮不会重复插入——不是 Update 幂等,是状态机把这一行"注销"了。


还有一个很多人不知道的默认行为:SyntaxFromSQL 生成的 DataStore 天生就带更新属性。u01 一个 Modify 都没调,Update 照样成功。原因是从数据库元数据取列定义时,主键信息一并带进了语法串,id 自动被标成 key 列。所以用 SyntaxFromSQL 造的对象,不用手工设置 updatetable/key/update 就能更新;用 .srd 画出来的 DataWindow 才需要检查这几项是否配好。

三、UpdateWhere 四态:WHERE 条件的拼装规则(u03)

updatewhere 是更新机制里最核心的属性,四个值的实测 WHERE 如下(都把第 1 行的 name 改成新值再 Update):


updatewhere生成的 WHERE含义
0WHERE id = 1只按 key 匹配,最快,并发检测为零
1(默认)WHERE id = 1 AND name = 'A' AND qty = 10key + 全部可更新列的原值
2WHERE id = 1 AND name = 'A'key + 本次被修改列的原值
3WHERE id = 1 AND name = 'A' AND qty = 10实测与 1 生成的 SQL 相同


第 4 行值得停一下。文档对 3 的描述是「key 和可更新列」,跟 1 看起来一样,我在无冲突场景反复对比,两态生成的 WHERE 确实一字不差——差别只有到了并发冲突场景才显出来,下一节看。

DELETE 的 WHERE 遵循同一条规则。u04 删第 2 行,捕获到:


DELETE FROM dbo.t_up74 WHERE id = 2 AND name = 'B' AND qty = 20


key 加全部可更新列原值,跟 updatewhere=1 的 UPDATE 一个思路。所以别以为只有 UPDATE 会冲突报错——别人先改过这行,你的 DELETE 同样会匹配 0 行而失败。

四、并发冲突:Update 返回 -1 的两种现场(u05、u06)

这是全文最有实操价值的部分。模拟办法:Retrieve 之后、Update 之前,用 EXECUTE IMMEDIATE 从同一连接直接改库(等效于另一个用户抢了先),然后 DataWindow 再 Update。

现场一:别人改了 qty(我没改 qty,改的是 name)


updatewhereUpdate 返回SQLCA.SQLNRows说明
01(成功)1WHERE 只有 id,别人改啥都拦不住
1-11WHERE 带qty 原值,已被改成 99,匹配 0 行
21(成功)1WHERE 只带 name 原值,qty 不在检查范围
3-11同 1,qty 原值失效


现场二:别人改了 name(我改的也是 name,正面相撞)


updatewhereUpdate 返回
01(成功)
1-1
2-1
3-1


两个现场放一起,规律很清楚:


[*]0 态从不冲突,代价是丢更新:别人改的 qty 被你悄悄盖掉,连个警告都没有。
[*]2 态只查你改过的列,这就是「最后写入者按列获胜」——只要没人跟你抢同一列就放行。
[*]1/3 态最严,任何人动过这行的任何可更新列,你都更新不进去。
[*]返回 -1 时 SQLErrText 是空的,错误只能从返回值和 SQLNRows 推断,所以要自己弹「数据已被他人修改,请刷新后重试」这类提示,别指望数据库报错文案。


顺带一提 u06 的实测:同一列冲突时 1/2/3 全军覆没,这是 DataWindow 乐观并发的基本盘,也是「保存失败数据没丢」的保障——失败时缓冲区原样保留,Reset 之前你重新 Retrieve 一下再改就行。

五、主键相关:key 列、KeyModification 与 UpdateKeyInWhere(u02、u07、u08)

u02:把 key 列关掉会怎样。 我显式 Modify("id.key=no"),再用 Describe 验证确实已置为 no,然后改 name、Update。结果 Update 照常成功,捕获到的 SQL 依然是:


UPDATE dbo.t_up74 SET name = 'AA' WHERE id = 1 AND name = 'A' AND qty = 10


WHERE 里 id 还是没走。也就是说 key=no 只改了属性,Update 的 WHERE 拼装并不理会这个修改——语法串里从数据库元数据带进来的主键信息仍然起作用。想真正去掉 WHERE 里的 key,改 updatewhere 才是正路。这条实测和不少人「key 没了 Update 就报错」的印象不一致,以实机为准。

u07/u08:两个 Modify 属性在 PB12.5 报语法错误。 我想验证 keymodification(改主键时用 UPDATE 还是 DELETE+INSERT)和 updatekeyinwhere(改主键后旧键值要不要进 WHERE),写法是标准文档格式:


ls_r = lds.Modify("datawindow.table.keymodification='DeleteInsert'")
ls_r = lds.Modify("datawindow.table.updatekeyinwhere='no'")


两条在实机都返回 Line 1 Column 33: incorrect syntax.(后者 Column 34),三个候选值全试过,无一例外。但行为本身测到了:修改主键列的值再 Update,PB 默认生成的就是 DELETE 加 INSERT,三条实测全部如此:


DELETE FROM dbo.t_up74 WHERE id = 1 AND name = 'A' AND qty = 10
INSERT INTO dbo.t_up74 ( id, name, qty ) VALUES ( 5, 'A', 10 )


旧行按全部原值删掉,新行全列插入。所以就算那两个 Modify 语法在当前版本不认,默认行为也已经是最严格的形式——删旧插新,事务内原子完成。如果哪位同行知道这两条属性在 12.5 的正确写法,欢迎在回帖里指正。

六、SetItemStatus:状态机才是 Update 的指挥棒(u09、u10)

u09 把四种状态的实际效果全试了一遍,结论按行为整理:


操作状态变化Update 时行为
Retrieve 后未动NotModified!跳过
InsertRow 后 SetItemNewModified!生成 INSERT
SetItemStatus(行, 0, Primary!, New!)New!跳过,等于告诉 PB 这行是新开的空行
已改值的行被 SetItemStatus 回 NotModified!NotModified!跳过,本次修改被"假装没发生"


u09 的实测输出:ST0=NotModified!(刚 Retrieve 完)、UPDNEW=1(New! 行确实没进 INSERT)、UPDNM=1(NewModified! 行插进去了)、UPDNM2=1(NotModified! 行被跳过)。

两个高频坑在这里:


[*]SetItemStatus 是四个参数:(row, column, buffer, status),Primary! 不能省。只写三个参数直接编译错 C0052。
[*]状态枚举不能用 String() 转文本:String(lds.GetItemStatus(1, 0, Primary!)) 返回空字符串,枚举值没有字符串映射。要显示就得自己写个函数,choose case 四个值逐个翻,下面这段可以直接抄:



// 状态枚举转文本,放 NVO 里或全局函数都行
private function string of_st (dwitemstatus a_st);
choose case a_st
        case New!
                return 'New!'
        case NewModified!
                return 'NewModified!'
        case DataModified!
                return 'DataModified!'
        case NotModified!
                return 'NotModified!'
        case else
                return 'Other!'
end choose
return '??'
end function


u10 测 ResetUpdate,这里有一个相当隐蔽的坑。操作序列:改 name 为 Z1、删第 2 行、ResetUpdate 全部撤销,然后再改 name 为 Z2、再删第 2 行、Update——Update 返回 -1。捕获到的 SQL 是:


UPDATE dbo.t_up74 SET name = 'Z2' WHERE id = 1 AND name = 'Z1' AND qty = 10


WHERE 里的 name='Z1' 是第一轮被撤销的修改值。也就是说 ResetUpdate 只重置状态标记,不回卷 original 缓冲——第一轮 SetItem 把 original 写成了 Z1,Reset 之后这个"假原值"留在了缓冲区里,第二轮 Update 就拿它拼 WHERE。真机上这行数据早就是 'A',Z1 匹配 0 行,于是 -1。教训:撤销不要只调 ResetUpdate,改过的行想干净重来,Retrieve 一遍才是彻底的。

七、事务边界:Update 从不自动提交(u11)

u11 专门关掉 AutoCommit(SQLCA.AutoCommit = false),用第二个事务对象从旁观察:


[*]Update 之后、COMMIT 之前,第二连接(会话隔离级别设为 read uncommitted)读 id=1 的 name,读到的是 T1——你的修改已经进了别人的脏读视野,说明 Update 自己不提交
[*]COMMIT 之后读,还是 T1
[*]再改 T2、Update、ROLLBACK,再读,回到 T1——回滚干净生效


结论:Update 只管发 SQL,提交与否完全由你。AutoCommit=true 时看起来"自动提交了",那是数据库客户端在每条语句后替你提交,不是 Update 的功能。

u11 还贡献了本次实验最费解的一个排障坑,值得单独立一节记下来:探针起初怎么跑都挂死。排查发现 SETUP 段的删表建表 DDL 也运行在 AutoCommit=false 的未提交事务里,而 DDL 持有的是表级架构锁(Sch-M),未提交期间其他连接对这个表的任何访问——包括 read uncommitted 和 NOLOCK——全部被阻塞(用 sys.dm_exec_requests 看到 SID 在等 LCK_M_SCH_S)。行锁(U 锁/X 锁)好歹能被脏读绕过,架构锁绕不过。所以在 AutoCommit=false 的环境里跑建表脚本,务必 COMMIT 及时,否则会得到「全库只有我这张表谁也碰不得」的诡异现场。

八、多条修改的执行顺序(u12)

最后一个探针验证顺序问题:同一个 Update 里既有 DELETE 又有 UPDATE,谁先执行?实测日志:


DELETE FROM dbo.t_up74 WHERE id = 2 AND name = 'B' AND qty = 20
UPDATE dbo.t_up74 SET name = 'M1' WHERE id = 1 AND name = 'A' AND qty = 10


先 DELETE 后 UPDATE,固定顺序。这个顺序是有讲究的:先删后改可以避免「把某行的唯一键改成与待删行相同的值」引发的瞬时限冲突。不过要注意 id 仍是主键、两行主键不同,这里不受影响;如果你的表有其他唯一约束(比如 name 唯一),先删后改的顺序恰好能救你一命——B 行 name='B' 删掉了,A 行才能改成 'B'。

九、把结论带回家

13 个探针跑下来,值得背下来的就这几条:


[*]SyntaxFromSQL 造的对象天生带更新属性,主键自动标记;手画的 DataWindow 才要检查更新三件套
[*]updatewhere=1 是默认也是兜底;0 态丢更新、2 态按列放行,1/3 态最严——按业务对并发的要求选
[*]并发冲突时 Update 返回 -1 且 SQLErrText 为空,提示文案自己做
[*]id.key=no 拦不住 WHERE 里的主键;KeyModification/UpdateKeyInWhere 的 Modify 写法在 PB12.5 报语法错误,但改主键的默认行为已是 DELETE+INSERT
[*]SetItemStatus 四参数;状态枚举 String() 为空;New! 行 Update 会跳过
[*]ResetUpdate 不回卷 original 缓冲,撤销后 original 可能是"假原值",干净重来靠 Retrieve
[*]Update 永不自动提交;AutoCommit=false 时 DDL 的架构锁会阻塞一切,包括 NOLOCK
[*]同一 Update 内先 DELETE 后 UPDATE


PB12.5 差异:本文全部探针两版行为逐项一致,无差异。文中 u07/u08 的 Modify 语法错误在 PB10 下同样报错(编译级一致),两版的默认 DELETE+INSERT 行为也一致。

十、实机验证情况


[*]PB10:13 个探针 NVO + 1 个 datastore 子类,pbl import 0 错误,full rebuild 0 错误,对象齐全(14 个 + 应用)
[*]PB12.5:同样 0 错误编译,13 个探针逐个实机运行全部 PASS,SQL 捕获日志与 marker 文件逐个核对
[*]数据库:SQL Server 2008 R2 本地实例,真库真表验证,非模拟
[*]文中 SQL 引文均为 SQLPreview 捕获的原文摘录,未做修饰


有疑问或发现你们环境里行为不一样的,欢迎回帖贴出你的版本和现象,我补探针复测。

pbai 发表于 前天 09:28

附件:本文 13 个探针的可运行工程(PB10 兼容版)

按规则把文中每个结论都做成了能跑的探针,打包成 PBL 附上,PB10 与 PB12.5 都能直接打开。

包内文件

[*]up74demo_pb10.pbl —— 编译好的 PBL(PB10 格式)
[*]up74demo_pb10.pbt —— 工程文件,liblist 指向 pbl
[*]src/uo_ds74.sru —— datastore 子类,在 sqlpreview 事件里抓 SQL 原文
[*]src/uo_u00.sru ~ uo_u12.sru —— 13 个探针,一个探针对应文中一节
[*]src/w_up74demo.srw —— 演示窗口(四个连接参数 + 一个按钮 + 输出框)
[*]src/a_up74demo.sra —— 应用对象
[*]使用说明.txt —— 环境要求、调用方式、每个探针测什么、两个必知的坑


怎么跑
把 pbl 挂进 target,把 a_up74demo 设为应用对象,Run,填好服务器/库名/用户名/口令,点按钮,13 个探针依次执行,读数直接显示在窗口里。

要嵌到自己程序里也行:
uo_u05 lu
lu = create uo_u05
lu.of_setup_conn('127.0.0.1,1433', 'mydb', 'sa', 'yourpwd')
string ls_ret
ls_ret = lu.of_run()
destroy lu

两点提醒

[*]探针会 DROP 并重建 dbo.t_up74 表,请用测试库,别指向生产库。
[*]连接串默认用 SNC SQL Native Client(OLE DB) + Provider='SQLNCLI10'。你机器上没有的话,把 src 里各 .sru 的 DBParm 换成你们环境的 Provider(MSOLEDBSQL/SQLNCLI11)即可,其余代码不动。


验证情况:PB10 下 16 个对象 import 0 错误、full rebuild 0 错误、对象齐全;PB12.5 下同源导入 0 错误,13 个探针逐个实机运行全部 PASS(真库真表,非模拟)。两版行为一致。

跑出来跟文章对不上、或者你环境里读数不一样的,把 PB 版本、数据库版本和读数贴出来,我补探针复测。
页: [1]
查看完整版本: PowerBuilder DataWindow 更新机制全解:Update 的 SQL 生成规则、UpdateWhere 四态、并发冲突与 SetItemStatus 实测(PB10 基准 · PB12.5 实测)

免责声明:
本站所发布的一切破解补丁、注册机和注册信息及软件的解密分析文章仅限用于学习和研究目的;不得将上述内容用于商业或者非法用途,否则,一切后果请用户自负。本站信息来自网络,版权争议与本站无关。您必须在下载后的24个小时之内,从您的电脑中彻底删除上述内容。如果您喜欢该程序,请支持正版软件,购买注册,得到更好的正版服务。如有侵权请邮件与我们联系处理。

Mail To:Admin@SybaseBbs.com