马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。
您需要 登录 才可以下载或查看,没有账号?站点注册
×
PowerBuilder 事务对象全解:SQLCA、CONNECT、AutoCommit、多事务对象与错误恢复(PB10 基准 · PB12.5 实测)
阅读说明
1. 适用版本:基准 PB10,兼容/实测 PB 12.5(连接串示例以 PB12.5 的 SNC 接口为准,PB10 用户替换 DBMS/DBParm 两行即可,事务语义两版一致)
2. 支持数据库:SQL Server 2008 R2(本文实测环境);事务语义同样适用于 Oracle、MySQL 等其它 DBMS,连接属性按各家接口替换
3. 操作系统与环境要求:Windows 7+;需要一个可连接的 SQL Server 2008+ 实例;文中服务器地址、账号、口令请替换为你自己环境的值
4. 难度系数:★★☆☆☆
5. 其它阅读说明:示例表 t_txn_demo 的建表 SQL 在第一节给出,照抄即可;建议先读过《PowerBuilder 嵌入式 SQL 实战》再读本文
写 PB 的人天天在用 SQLCA,但很多人停留在「会 connect 会 commit」的程度:AutoCommit 改成 true 会发生什么、两个事务对象怎么共存、出错以后连接处于什么状态、SQLNRows 到底数的是什么——这些真到出问题时才临时翻文档。本文把「事务」这一层一次讲透,全部结论来自真实连接 SQL Server 2008 R2 的实机运行,文中每一组读数都有对应探针验证过。
一、事务对象是什么
事务对象(transaction)是 PB 与数据库之间的一条「连接 + 事务上下文」。你写的每一条嵌入式 SQL、每一个 DataWindow 的 Retrieve/Update,都要通过某个事务对象送到数据库。
PB 应用启动时自带一个预声明好的事务对象 SQLCA(SQL Communications Area),不用 create 就能用。但 SQLCA 只是「一个实例」,不是「唯一选择」——transaction 是一种数据类型,你可以声明自己的事务变量:
- transaction lt1 // 声明,此时 lt1 是空引用
- transaction lt2
- lt1 = create transaction // 必须 create 以后才能用,否则运行时错误
- lt2 = create transaction
复制代码
本文演示表只有一个,两条建表语句照抄执行即可:
- create table t_txn_demo (
- id int primary key,
- name varchar(50),
- amount int null
- )
复制代码
事务对象的主要属性分两组:连接属性(CONNECT 之前设置)和状态属性(执行后读取)。
| 分组 | 属性 | 说明 | | 连接 | DBMS | 接口名,如 SNC SQL Native Client(OLE DB) | | 连接 | ServerName | 服务器地址,SQL Server 端口写法为「地址,端口」 | | 连接 | Database | 库名 | | 连接 | LogId / LogPass | 登录账号 / 口令 | | 连接 | AutoCommit | 是否自动提交,默认 false | | 连接 | DBParm | 接口专用参数串 | | 状态 | SQLCode | 最近一条语句的结果:0 成功 / -1 失败 / 100 无行 | | 状态 | SQLDBCode | 数据库原生错误码 | | 状态 | SQLErrText | 数据库原生错误文本 | | 状态 | SQLNRows | 最近一条语句影响的行数 |
注意上表里「最近一条语句」六个字——后面第五节会看到 SQLNRows 的一个大坑就出在这六个字上。
二、连接:CONNECT 与错误属性
标准连接写法——属性逐项设置,然后 CONNECT,立即检查 SQLCode:
- // 连接属性:按自己环境替换服务器、账号、口令
- SQLCA.DBMS = 'SNC SQL Native Client(OLE DB)' // PB12.5 的 SQL Server 接口
- SQLCA.ServerName = '127.0.0.1,1433' // 服务器地址,端口(逗号分隔)
- SQLCA.Database = 'mydb' // 库名
- SQLCA.LogId = 'sa' // 登录账号
- SQLCA.LogPass = '你的口令' // 口令
- SQLCA.AutoCommit = false // 显式声明,别依赖默认值
- connect using SQLCA; // 建立连接
- if SQLCA.SQLCode <> 0 then
- MessageBox('连接失败', '错误码=' + String(SQLCA.SQLDBCode) + '~n' + SQLCA.SQLErrText)
- return
- end if
复制代码
三个实测细节:
- AutoCommit 默认就是 false(实测:create 一个全新事务对象,读 AutoCommit 得 false),但写代码时建议显式赋值——读到代码的人不需要去查默认值。
- SQLCA 已连接状态下再执行 CONNECT 会失败(实测:连接建立后不 DISCONNECT 直接再 connect,SQLCode 非 0)。所以「重连」的正确姿势是先 disconnect 再 connect,本文示例对象 of_connect 就是这么写的。
- connect 失败时错误属性立即可读。用错误口令实测:SQLCode=-1、SQLDBCode=18456(SQL Server 的登录失败错误码)、SQLErrText 长度 60 字符(含 SQLSTATE 与原因描述)。错误文本不要弹给最终用户,记录到日志即可。
三、AutoCommit:每一句话各自提交
AutoCommit 是最容易被误解的属性。false(默认)时,你的语句在一个隐式事务里,COMMIT 才落盘、ROLLBACK 可撤销;true 时,每条语句一执行完立刻提交,没有「未提交」状态可言。
实测对照(两个连接同时看着同一张表):
- lt 的 AutoCommit = true,插入一行 id=90:返回 SQLCode=0;
- 同一时刻,另一条连接(AutoCommit=false)count(*) 立即数到这一行——插入已经提交,不需要任何 commit 语句;
- 随后对 lt 执行 rollback using lt;——不报错,SQLCode=0,且实测再查那一行,count 仍然数得到它:rollback 没有产生任何效果。
最后一条值得单独强调:AutoCommit=true 时 ROLLBACK 是「不报错也不生效」的。代码写错了没有异常、没有错误码,你以为回滚了,其实什么都没发生。这种静默失效比报错可怕得多——所以 AutoCommit=true 的场景要格外小心,凡涉及多步一致性写入,一律用默认的 false。
另一个实测陷阱:建库语句不允许在隐式事务里跑。AutoCommit=false 下执行 create database 直接报错「在多语句事务内不允许使用 CREATE DATABASE 语句」。正确做法是连接 master 时临时把 AutoCommit 设为 true,建完库再断开、改回 false 连业务库:
- // 建库必须 AutoCommit=true:隐式事务内 CREATE DATABASE 会被 SQL Server 拒绝
- SQLCA.AutoCommit = true
- SQLCA.Database = 'master'
- connect using SQLCA;
- execute immediate "if db_id('mydb') is null create database mydb" using SQLCA;
- disconnect using SQLCA;
- SQLCA.AutoCommit = false // 业务库回到默认事务模式
- SQLCA.Database = 'mydb'
- connect using SQLCA;
复制代码
四、COMMIT / ROLLBACK 与隔离性
AutoCommit=false 下,语句之间靠 COMMIT/ROLLBACK 划界。同一事务对象执行的插入,在提交之前对其它连接不可见——这是数据库 READ COMMITTED 隔离级别的行为,实测确认:
- 连接 A(SQLCA)插入 id=60 一行,不 commit;
- 连接 B(另一个事务对象)count(*),读到的还是 2 行(看不到 id=60);
- A 执行 commit using SQLCA;;
- B 再 count(*),读到 3 行。
然后是 ROLLBACK 的撤销范围:它撤销的是「自上次 COMMIT 以来的所有未提交语句」,不是最近一条。实测:提交 id=60 之后又插入 id=61,再 rollback——表里是 3 行(id=60 在),id=61 没了。 rollback 不能「只撤销一条」。
还有一个连带行为要提醒:未提交的插入持有行锁,其它连接对这张表的普通查询会被阻塞(我们的探针第一版就在这里挂死了,后来改用 SQL Server 的 readpast 提示跳过锁定行才顺利完成测试)。实测环境下这个阻塞没有超时上限,也就是说一个忘 commit 的长事务可以把全组人都拖住。写长事务时控制好粒度,用完尽快 commit。
- // 隔离性演示:连接 B 看不到连接 A 未提交的数据
- long ll_cnt
- // —— 连接 A(SQLCA)已插入一行,尚未 commit ——
- // 连接 B(lt2)视角
- select count(*) into :ll_cnt from t_txn_demo using lt2;
- MessageBox('B 视角', '提交前=' + String(ll_cnt)) // 不含 A 未提交的行
- commit using SQLCA; // A 提交
- select count(*) into :ll_cnt from t_txn_demo using lt2;
- MessageBox('B 视角', '提交后=' + String(ll_cnt)) // 多了一行
复制代码
五、SQLNRows:它数的是「最近一条语句」
SQLNRows 的坑最隐蔽。它不是「本次事务一共改了多少行」,而是最近一条 SQL 语句影响的行数。实测:
- 一条 insert ... select ... union all select ... union all select ... 一次插 3 行:SQLNRows=3;
- 改成循环 3 次、每次插 1 行:循环结束后 SQLNRows=1(只有最后一条的数字)。
所以「统计本次一共插入多少行」不能靠 SQLNRows 累计,要么自己加计数器,要么查 @@rowcount 语义的等价物。同理,每执行一条嵌入式 SQL,SQLCode 也会被刷新——检查 SQLCode 必须紧跟在对应语句后面,不能隔好几条再查。
SQLCode=100 是「fetch 没取到行」,只出现在游标 FETCH 之后。实测游标场景:WHERE 条件查不到行时 fetch 后 SQLCode=100,正常取到行时 SQLCode=0。100 不是错误,是游标循环的正常出口,写成 if SQLCA.SQLCode = 100 then exit 之类的判断即可。
UPDATE/DELETE 的 SQLNRows 还有第三个用途:判断语句到底改没改到行。UPDATE ... WHERE id = 999 语法完全正确、SQLCode=0,但 SQLNRows=0——你更新的是一个不存在的账户。第九节的转账示例就靠这个校验拦截「转到不存在的账户」。
六、出错之后:连接处于什么状态
主键冲突是最容易制造的数据库错误,实测 id=1 已存在时再插 id=1:
- SQLCode=-1、SQLDBCode=2627(SQL Server 主键冲突错误码)、SQLErrText 含完整描述;
- 不做 rollback,紧接着再执行下一条 insert:SQLCode=0,成功了。
第三条出乎很多人意料——在 SQL Server 默认配置下,单条语句的约束冲突不会终止整个事务,连接还能继续干活。但强烈建议把「出错即 rollback」当成纪律:一是出错的那条语句引发的半成品改动需要回滚,二是不同 DBMS 对「出错后事务是否继续」规定不同(有的数据库直接进入 abort 状态,后续语句全部拒绝),跨库的代码不能依赖这个「宽容」。
- // 出错恢复的标准流程:检查 → rollback → 重试或上报
- execute immediate "insert into t_txn_demo values (1, 'dup', null)" using SQLCA;
- if SQLCA.SQLCode <> 0 then
- rollback using SQLCA; // 先把出错事务清干净
- MessageBox('插入失败', '错误码=' + String(SQLCA.SQLDBCode) + '~n' + SQLCA.SQLErrText)
- return
- end if
- commit using SQLCA;
复制代码
rollback 之后实测立即重插同一批数据,SQLCode=0、SQLNRows=1——连接完全恢复,无需重连。
七、多事务对象:双库连接与「各自为政」
SQLCA 之外可以 create 任意多个事务对象,每个是一条独立连接,各自有自己的 SQLCode/SQLNRows 和自己的事务边界:
- transaction lt1
- long ll_cnt
- datastore lds
- string ls_syn, ls_err
- lt1 = create transaction
- lt1.DBMS = 'SNC SQL Native Client(OLE DB)'
- lt1.ServerName = '127.0.0.1,1433' // 换成另一台服务器即「双库连接」
- lt1.Database = 'otherdb'
- lt1.LogId = 'sa'
- lt1.LogPass = '你的口令'
- lt1.AutoCommit = false
- connect using lt1;
- if lt1.SQLCode <> 0 then
- MessageBox('第二连接失败', lt1.SQLErrText)
- return
- end if
- // lt1 里未提交的写,SQLCA 那条连接看不到(隔离性按连接生效)
- execute immediate "insert into t_txn_demo values (80, 'lt1_pend', null)" using lt1;
- select count(*) into :ll_cnt from t_txn_demo using SQLCA;
- // 此刻 ll_cnt 不含 lt1 未提交的那行
- commit using lt1; // 只提交 lt1 的改动,SQLCA 不受影响
- // DataWindow/DataStore 也能挂到自定义事务对象上
- ls_syn = lt1.SyntaxFromSQL("select id, name, amount from t_txn_demo order by id", "style(type=grid)", ls_err)
- lds = create datastore
- lds.Create(ls_syn)
- lds.SetTransObject(lt1) // 挂 lt1,检索走这条连接
- MessageBox('行数', String(lds.Retrieve()))
- destroy lds
- disconnect using lt1;
- destroy lt1
复制代码
三个实测要点:
- 事务对象的方法可以直接调用:lt1.SyntaxFromSQL(...) 在 lt1 这条连接上生成 DataWindow 语法,不用碰全局函数。
- SetTransObject(lt1) 之后 DataStore 的 Retrieve/Update 全走 lt1——这就是「双库操作」的基石:主库用 SQLCA,报表库用 lt1,各查各的。
- commit/rollback 只作用于 USING 指定的那个事务对象。commit using lt1 不会把 SQLCA 的未提交改动一起提交。
对象层的行为也实测了一遍:create transaction 后逐项设置属性再回读,全部一致;AutoCommit 默认 false,赋 true 后读回 true,改回 false 读回 false;ClassName(SQLCA) 返回 transaction。而两个负向用例都是运行时错误 2(Null object reference):没 create 就给属性赋值、destroy 之后再访问属性,都能被 TRY-CATCH(runtimeerror) 捕获。所以事务变量用之前判一下 IsValid,或者干脆在 create 之后立刻用,destroy 之后不再碰。
八、完整示例:nvo_txn_demo(转账演示)
把上面的要点拼成一个完整可导入的对象。「转账」是事务教学的经典场景:两步 UPDATE 必须同生共死,任何一步失败就整体回滚。
- $PBExportHeader$nvo_txn_demo.sru
- forward
- global type nvo_txn_demo from nonvisualobject
- end type
- end forward
- global type nvo_txn_demo from nonvisualobject
- end type
- global nvo_txn_demo nvo_txn_demo
- type variables
- end variables
- forward prototypes
- public function integer of_connect ()
- public function integer of_transfer (long al_from, long al_to, long al_amt)
- public function long of_balance (long al_id)
- public subroutine of_disconnect ()
- end prototypes
- // 连接:先断开旧连接(已连接状态下直接 CONNECT 实测会失败),再逐项设置属性
- public function integer of_connect ();
- disconnect using SQLCA;
- SQLCA.DBMS = 'SNC SQL Native Client(OLE DB)'
- SQLCA.ServerName = '127.0.0.1,1433'
- SQLCA.Database = 'mydb'
- SQLCA.LogId = 'sa'
- SQLCA.LogPass = '你的口令'
- SQLCA.AutoCommit = false
- connect using SQLCA;
- if SQLCA.SQLCode <> 0 then
- return 0
- end if
- return 1
- end function
- // 转账:al_from 转 al_amt 给 al_to。成功返回 1,金额非法返回 -1,
- // 任一步 UPDATE 失败或未更新到行则回滚并返回 -2。
- public function integer of_transfer (long al_from, long al_to, long al_amt);
- if al_amt <= 0 then
- return -1
- end if
- UPDATE t_txn_demo SET amount = amount - :al_amt WHERE id = :al_from USING SQLCA;
- if SQLCA.SQLCode <> 0 or SQLCA.SQLNRows <> 1 then
- rollback using SQLCA;
- return -2
- end if
- UPDATE t_txn_demo SET amount = amount + :al_amt WHERE id = :al_to USING SQLCA;
- if SQLCA.SQLCode <> 0 or SQLCA.SQLNRows <> 1 then
- rollback using SQLCA;
- return -2
- end if
- commit using SQLCA;
- return 1
- end function
- // 读某账户余额,出错返回 -99999
- public function long of_balance (long al_id);
- long ll_amt
- select amount into :ll_amt from t_txn_demo where id = :al_id using SQLCA;
- if SQLCA.SQLCode <> 0 then
- return -99999
- end if
- return ll_amt
- end function
- public subroutine of_disconnect ();
- disconnect using SQLCA;
- end subroutine
复制代码
调用方式(余额初始 100 / 200):
- nvo_txn_demo ldm
- long ll_a, ll_b
- ldm = create nvo_txn_demo
- if ldm.of_connect() <> 1 then
- MessageBox('连接失败', '请检查连接属性')
- return
- end if
- ll_a = ldm.of_balance(1) // 100
- ll_b = ldm.of_balance(2) // 200
- ldm.of_transfer(1, 2, 30) // 返回 1:转账成功
- ll_a = ldm.of_balance(1) // 70
- ll_b = ldm.of_balance(2) // 230
- ldm.of_transfer(1, 2, 0) // 返回 -1:金额非法,直接拒绝
- ldm.of_transfer(1, 999, 5) // 返回 -2:999 账户不存在,第一步已回滚
- ll_a = ldm.of_balance(1) // 仍然是 70,分文未少
- ldm.of_disconnect()
- destroy ldm
复制代码
第三组读数值得看一眼:转到不存在的账户 999 时,第一步 UPDATE(id=1 减 5)已经执行了,但第二步查到 SQLNRows=0,函数立刻 rollback——id=1 的余额实测回到 70,两步要么都成、要么都不算,这就是事务的意义。
九、PB12.5 差异
全部用例在 PB10 与 PB12.5 双轨各跑一遍:对象层用例(属性回读、AutoCommit 默认值、destroy 后访问、未 create 使用、ClassName)两版输出逐字节一致;数据库用例(连接、错误属性、隔离性、SQLNRows、错误恢复、DataStore 挂载)在 PB12.5 全部通过,PB10 侧因运行包不含 SNC 接口按设计跳过(事务语义与接口无关,连接串按自家接口替换即可)。结论:PB12.5 与 PB10 的事务对象行为一致,无版本差异。
唯一的环境提醒在连接串本身:SNC 是 PB11+ 才有的接口,PB10 连 SQL Server 请改用 OLE DB 或 MSS 接口的 DBMS/DBParm 写法,本文第二节的属性逐项设置流程不变。
十、坑清单(13 条)
- SQLNRows 数的是最近一条语句,循环 3 次单行 insert 后仍是 1(实测)。
- 已连接状态下再 CONNECT 会失败,重连前必须先 DISCONNECT(实测)。
- AutoCommit=false 下 CREATE DATABASE 报「在多语句事务内不允许」,建库要临时 AutoCommit=true(实测)。
- AutoCommit=true 时 ROLLBACK 不报错也不回滚,静默失效(实测)。
- 未 create 的 transaction 赋属性或 CONNECT:运行时错误 2(Null object reference),TRY-CATCH 可捕(实测)。
- destroy 之后再访问属性:同样运行时错误 2(实测)。
- EXECUTE IMMEDIATE 不接受字符串拼接表达式(C0031),必须先拼进变量再 execute immediate :ls_sql(实测)。
- SyntaxFromSQL 的正确写法是事务对象方法 lt.SyntaxFromSQL(sql, style, err),写成全局函数 SyntaxFromSQL(trans, sql, err) 报 C0084(实测)。
- 错误码实测:登录失败 SQLDBCode=18456、主键冲突 SQLDBCode=2627,记下来排错快。
- SQLCode=100 是游标 FETCH 无行的正常出口,不是错误。
- 未提交的插入会让其它连接的查询阻塞且无超时,长事务尽快 commit。
- SQL Server 默认单语句错误不终止事务(出错后还能继续执行),但跨库代码一律按「出错即 rollback」处理。
- create transaction 后 AutoCommit 默认 false;但所有属性建议显式赋值,别依赖默认值。
十一、实机验证情况
本文全部代码经双轨实机验证:
- PB10:对象源码导入 0 错误、全量重建 0 错误;对象层用例(事务对象属性回读、AutoCommit 默认值、负向用例)在 PB10 运行时真实跑通,断言 9/9 全中,与 PB12.5 输出逐字节一致;
- PB12.5:导入 0 错误、全量重建 0 错误;数据库用例(连接与错误属性、SQLNRows、隔离性、错误恢复、多事务对象、DataStore 挂载、AutoCommit 行为、完整转账示例)47 条硬断言全部命中;
- 静态检查 0 错误 0 提醒,源码编码检查全部通过。
文中的每一组读数(18456、2627、SQLNRows=1/3/6、隔离性计数、转账余额)都来自真实运行,未实测的写法没有写入正文。
一句话总结:事务对象的语法半小时就能背下来,真正值钱的是那些「不报错但不干活」的边界——AutoCommit=true 下的 ROLLBACK、已连接下的重复 CONNECT、SQLNRows 的计数口径。把这三条记住,你的连接代码就比大多数人可靠了。 |