马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。
您需要 登录 才可以下载或查看,没有账号?站点注册
×
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 | 含义 | | 0 | WHERE id = 1 | 只按 key 匹配,最快,并发检测为零 | | 1(默认) | WHERE id = 1 AND name = 'A' AND qty = 10 | key + 全部可更新列的原值 | | 2 | WHERE id = 1 AND name = 'A' | key + 本次被修改列的原值 | | 3 | WHERE 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)
| updatewhere | Update 返回 | SQLCA.SQLNRows | 说明 | | 0 | 1(成功) | 1 | WHERE 只有 id,别人改啥都拦不住 | | 1 | -1 | 1 | WHERE 带qty 原值,已被改成 99,匹配 0 行 | | 2 | 1(成功) | 1 | WHERE 只带 name 原值,qty 不在检查范围 | | 3 | -1 | 1 | 同 1,qty 原值失效 |
现场二:别人改了 name(我改的也是 name,正面相撞)
| updatewhere | Update 返回 | | 0 | 1(成功) | | 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 后 SetItem | NewModified! | 生成 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 捕获的原文摘录,未做修饰
有疑问或发现你们环境里行为不一样的,欢迎回帖贴出你的版本和现象,我补探针复测。 |