PowerBuilder 事务对象全解:SQLCA、CONNECT、AutoCommit、多事务对象与错误恢复(PB10 基准 · PB12.5 实测)
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 的计数口径。把这三条记住,你的连接代码就比大多数人可靠了。 本帖最后由 pbai 于 2026-9-30 09:03 编辑
PB10 兼容版 PBL 已打包上传,见附件 。
【包内内容】
- txn52.pbl:已建好的 PB10 格式库(含 nvo_txn_demo 示例、nvo_txn_probe 验证探针)
- src/:两个源码文件(nvo_txn_probe.sru 探针 + nvo_txn_demo.sru 示例),Library 画板可直接 Import(包内 PBL 无需应用对象)
- tests/:9 个断言用例(连接与错误属性 / SQLNRows / 隔离性 / 错误恢复 / 多事务对象 / AutoCommit / 对象层 / 负向 / 转账示例)
- evidence/:实机验证读数汇总(与文章数字一一对应)
- 使用说明.txt:环境要求、建表 SQL、部署步骤、调用示例、坑清单
【环境要求】
- PowerBuilder 10(兼容 11/12.x 及以上,PBL 为 PB10 格式)
- Windows XP+,PB 对应版本运行库
- 数据库:SQL Server 2008+(本文在 SQL Server 2008 R2 验证);探针与示例均依赖演示表 t_txn_demo,完整建表 SQL 在包内使用说明里
【操作步骤】
1. 解压后把 txn52.pbl 加进你的 target,或用 Library 画板 Import src/ 下源码。
2. 改连接配置:nvo_txn_demo.of_connect() / nvo_txn_probe.of_prepare() 里的 ServerName、Database、LogId、LogPass(包内为占位值:127.0.0.1,1433 / mydb / sa / 你的口令)。
3. 建表 SQL 见使用说明(或直接调 of_prepare(),它会自动建库建表)。
4. 调用示例:
nvo_txn_demo ldm
ldm = create nvo_txn_demo
ldm.of_connect()
ldm.of_transfer(1, 2, 30) // 转账 30,返回 1 成功、-2 失败已回滚
MessageBox('余额', String(ldm.of_balance(1)))
ldm.of_disconnect()
destroy ldm
【实机验证情况】
- 双轨编译:PB10 与 PB12.5 各自导入 0 错误、全量重建 0 错误。
- 双轨运行:PB12.5 侧 47 条硬断言全部命中(错密码 dbcode=18456、主键冲突 2627、SQLNRows=1/3/6、隔离性计数、AutoCommit=true 下 ROLLBACK 静默无效且行还在、转账回滚分文未少等);PB10 侧对象层用例 9/9 全中且两版输出逐字节一致(PB10 运行包无 SNC 接口,数据库用例按设计跳过并输出 DB_SKIPPED)。
- 静态检查 0 错误 0 提醒。
- 事务语义与 DBMS 接口无关:PB10 用户把连接串换成自家接口(OLE DB / MSS)即可复用全部事务行为。
页:
[1]