马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。
您需要 登录 才可以下载或查看,没有账号?站点注册
×
本帖最后由 pbai 于 2026-10-30 06:14 编辑
PowerBuilder 调用存储过程全解:DECLARE PROCEDURE、FETCH 取结果集、OUTPUT 参数与 RPCFUNC 实测(PB10 基准 · PB12.5 实测)
阅读说明
1. 适用版本:基准 PB10;兼容/实测:PB 12.5
2. 支持数据库:MSSQL(SQL Server 2005+,示例在 SQL Server 2008 R2 实测;走 SNC 接口)
3. 操作系统与环境要求:Windows 7+;需要能连上 SQL Server 的客户端环境(SNC 接口依赖 SQL Server Native Client 10.0)
4. 难度系数:★★★☆☆
5. 其它阅读说明:示例表与存储过程的建表 SQL 在第三节,可整段照抄执行;动态 SQL 四种形态不在本文范围(另有专文)
PB 里调存储过程,一共就三条路:嵌入式 DECLARE ... PROCEDURE FOR、动态 SQL 第四形态、RPCFUNC(在事务对象的标准类用户对象里声明过程)。前两条路取结果集没问题,但我在 PB12.5 实机上把每一格都踩了一遍之后发现,输入参数、输出参数、返回码这三样,三条路的表现完全不是一回事:嵌入式路线的 OUTPUT 参数编译放行、执行也"成功",但值就是拿不到;返回码也一样。想要输出参数和 RETURN 值,只有 RPCFUNC 一条正路。
本文所有结论都是在 PB12.5 + SQL Server 2008 R2 上真跑出来的读数,不是抄文档。PB10 侧做了全量编译验证(详见第十二节)。
一、准备:测试表与存储过程
先建一张测试表,再建七个示例过程。下面这段 SQL 可以直接在 SSMS 里执行,也可以在 PB 里用 EXECUTE IMMEDIATE 一条条跑(我的探针就是这么准备的)。
- create table dbo.t_s82(id int primary key, name nvarchar(10), qty int);
- insert into dbo.t_s82 values(1,'A',10);
- insert into dbo.t_s82 values(2,'B',20);
- insert into dbo.t_s82 values(3,'C',30);
- create procedure dbo.sp_s82_rs as
- begin
- select id, name, qty from dbo.t_s82 order by id
- end
- create procedure dbo.sp_s82_in @id int as
- begin
- select id, name, qty from dbo.t_s82 where id = @id
- end
- create procedure dbo.sp_s82_out
- @id int, @name nvarchar(10) output, @qty int output
- as
- begin
- select @name = name, @qty = qty from dbo.t_s82 where id = @id
- return @qty * 10
- end
- create procedure dbo.sp_s82_ret @x int as
- begin
- return @x * 2
- end
- create procedure dbo.sp_s82_dml @id int, @qty int as
- begin
- update dbo.t_s82 set qty = @qty where id = @id
- return @@rowcount
- end
- create procedure dbo.sp_s82_mix @id int as
- begin
- update dbo.t_s82 set qty = qty + 1 where id = @id
- select id, name, qty from dbo.t_s82 where id = @id
- end
- create procedure dbo.sp_s82_two as
- begin
- select id from dbo.t_s82 where id <= 2
- select name from dbo.t_s82 where id = 3
- end
复制代码
七个过程各管一件事:sp_s82_rs 返回结果集;sp_s82_in 带输入参数;sp_s82_out 带两个 OUTPUT 参数还带 RETURN;sp_s82_ret 只返回 RETURN 码;sp_s82_dml 只做 UPDATE;sp_s82_mix 是「DML + SELECT」混合体;sp_s82_two 一次返回两个结果集。
二、路线一:嵌入式 DECLARE 取结果集
嵌入式路线的完整动作是四步:DECLARE 声明、EXECUTE 执行、FETCH 取行、CLOSE 关闭。先看最基础的——调一个只返回结果集的过程,把三行数据全部取出来:
- // 连接参数:请换成你自己的环境
- SQLCA.DBMS = 'SNC SQL Native Client(OLE DB)'
- SQLCA.LogId = '你的登录名'
- SQLCA.LogPass = '你的口令'
- SQLCA.ServerName = '你的服务器,端口'
- SQLCA.Database = '你的库名'
- SQLCA.AutoCommit = false
- SQLCA.DBParm = "Provider='SQLNCLI10',DataSource='你的服务器,端口'"
- CONNECT USING SQLCA;
- if SQLCA.SQLCode <> 0 then
- MessageBox('连接失败', SQLCA.SQLErrText)
- return
- end if
- // 声明并执行存储过程(无参数过程,DECLARE 直接写过程名)
- string ls_name
- long ll_id, ll_qty, ll_k
- ll_k = 0
- DECLARE c1 PROCEDURE FOR dbo.sp_s82_rs;
- EXECUTE c1;
- // 关键读数:EXECUTE 成功返回 0,但 SQLNRows 是 0,不是行数
- FETCH c1 INTO :ll_id, :ls_name, :ll_qty;
- DO WHILE SQLCA.SQLCode = 0
- ll_k = ll_k + 1
- FETCH c1 INTO :ll_id, :ls_name, :ll_qty;
- LOOP
- CLOSE c1;
- MessageBox('取到行数', String(ll_k) + ' 行,末次 SQLCode=' + String(SQLCA.SQLCode))
复制代码
实测读数:EXECUTE 后 SQLCode=0、SQLNRows=0(存储过程的结果集行数不进 SQLNRows,别拿它判行数);FETCH 循环取到 1,A,10 / 2,B,20 / 3,C,30 三行;结果集耗尽时 SQLCode=100;CLOSE 后 SQLCode=0。
两个细节值得单独说。第一,FETCH 的 INTO 列表必须和结果集列一一对应,类型按 PB 侧声明:int 对 long、nvarchar 对 string。第二,CLOSE 不是可有可无的——不 CLOSE,这条声明占着的语句句柄就一直在,同一事务对象里声明多了会互相干扰。
三、带输入参数:@参数名 = :变量名
有参数的过程,DECLARE 时用「@参数名 = :变量名」的格式传值(PB 变量前加冒号):
- // 输入值在前段定义并赋值
- long ll_id // 要查的行 id
- ll_id = 2
- DECLARE c2 PROCEDURE FOR dbo.sp_s82_in @id = :ll_id;
- EXECUTE c2;
- string ls_name
- long ll_qty
- FETCH c2 INTO :ll_id, :ls_name, :ll_qty;
- if SQLCA.SQLCode = 0 then
- MessageBox('查询结果', String(ll_id) + ',' + ls_name + ',' + String(ll_qty))
- else
- MessageBox('没取到', 'SQLCode=' + String(SQLCA.SQLCode))
- end if
- CLOSE c2;
复制代码
实测取回 2,B,20,一步到位。变量绑定发生在 EXECUTE 时刻,所以先 DECLARE 后赋值也能工作,但建议统一「先赋值、再 DECLARE」,读代码的人不容易糊涂。
四、双结果集:FETCH 会自动接续下一个结果集
sp_s82_two 返回两个结果集:第一个是 id 列两行,第二个是 name 列一行。实测最有意思的结论在这里:第一个结果集取到 SQLCode=100 之后,不用重新 EXECUTE,直接继续 FETCH,会自动落到第二个结果集上:
- // 输入值:无参数过程,直接声明
- long ll_id
- string ls_name
- DECLARE c9 PROCEDURE FOR dbo.sp_s82_two;
- EXECUTE c9;
- FETCH c9 INTO :ll_id; // 第一个结果集:1
- FETCH c9 INTO :ll_id; // 第一个结果集:2
- FETCH c9 INTO :ll_id; // 第一个结果集耗尽,SQLCode=100
- FETCH c9 INTO :ls_name; // 自动接续第二个结果集,读到 'C'
- MessageBox('第二个结果集首行', ls_name)
- CLOSE c9;
复制代码
实测读数序列:r1=1、r2=2、endcode=100、strfetch=0 name=C(跨结果集的这次 FETCH 返回 0!)、再往后 FETCH 恒为 100。也就是说,100 在这里是「当前结果集取完」的信号,不是「整个声明结束」的信号。要想知道还有没有下一个结果集,直接再 FETCH 一次就行。
五、OUTPUT 参数:嵌入式路线拿不到值
这是我本轮最重要的实测结论之一。sp_s82_out 有两个 OUTPUT 参数,嵌入式路线写起来完全合法:
- // 输入值在前段定义:要查的 id;两个输出参数先声明
- long ll_id, ll_qty
- string ls_name
- ll_id = 3
- ll_qty = 0
- ls_name = Fill(' ', 10)
- DECLARE c3 PROCEDURE FOR dbo.sp_s82_out @id = :ll_id, @name = :ls_name OUTPUT, @qty = :ll_qty OUTPUT;
- EXECUTE c3;
- // 执行返回 0,看起来一切正常——
- MessageBox('执行后', 'name_len=' + String(Len(ls_name)) + ' qty=' + String(ll_qty))
- CLOSE c3;
复制代码
实测读数:EXECUTE 返回 0、无任何报错,但 name_len=10(还是我预填的 10 个空格)、qty=0(还是初值)——输出参数根本没被回填。紧接着 FETCH 返回 -1,因为这个过程没有结果集可取。
也就是说:嵌入式 DECLARE 的 OUTPUT 参数,编译放行、执行成功、值不回来。这条语法在 SNC 接口下就是个「安静的哑巴」。如果你的过程靠 OUTPUT 参数往外传值,嵌入式路线走不通,得换 RPCFUNC(第七节)。
六、RETURN 返回码:嵌入式路线同样拿不到
sp_s82_ret(@x) 只做一件事:RETURN @x * 2。嵌入式路线执行它:
- // 输入值:传给 @x 的数
- long ll_x
- ll_x = 21
- DECLARE c4 PROCEDURE FOR dbo.sp_s82_ret @x = :ll_x;
- EXECUTE c4;
- // 实测:EXECUTE 后 SQLCode=100(没有结果集),FETCH 返回 -1,RETVAL 无从谈起
- MessageBox('执行码', String(SQLCA.SQLCode))
- CLOSE c4;
复制代码
实测:EXECUTE 后 SQLCode=100(表示「没有结果集」),FETCH 返回 -1,没有任何通道能拿到 RETURN 的 42。存储过程的返回码,嵌入式路线从设计上就不支持——这正是 RPCFUNC 存在的理由之一。
顺带一个重要读数:DML 类过程(如 sp_s82_dml 只做 UPDATE)在嵌入式路线下,EXECUTE 后 SQLCode 也是 100 不是 0。所以判存储过程执行成败,不能简单写「SQLCode 不等于 0 就报错」——100 是正常的「无结果集」,-1 才是出错。
七、路线二:RPCFUNC——输出参数与返回码的正解
RPCFUNC 不是写在普通对象里的,它要写在一个继承自 transaction(事务对象)的标准类用户对象里。先建一个标准类用户对象 uo_trans_sp,继承 transaction,然后在它的局部外部函数区声明:
- // 在标准类用户对象(继承 transaction)的
- // 「Declare Local External Functions」里声明:
- function long sp_s82_out (long al_id, ref string as_name, ref long al_qty) RPCFUNC ALIAS FOR "dbo.sp_s82_out"
- function long sp_s82_ret (long al_x) RPCFUNC ALIAS FOR "dbo.sp_s82_ret"
复制代码
声明的读法:函数返回类型对应过程的 RETURN 码;ref 参数对应 OUTPUT 参数;RPCFUNC 表示这是数据库过程调用;ALIAS FOR 写数据库里的真名。然后用这个类的实例连接数据库、直接像调函数一样调过程:
- // 输入值在前段定义:要查的 id 和预声明的输出变量
- long ll_id, ll_qty, ll_rc
- string ls_name
- uo_trans_sp ltr
- ltr = create uo_trans_sp
- ltr.DBMS = 'SNC SQL Native Client(OLE DB)'
- ltr.LogId = '你的登录名'
- ltr.LogPass = '你的口令'
- ltr.ServerName = '你的服务器,端口'
- ltr.Database = '你的库名'
- ltr.AutoCommit = false
- ltr.DBParm = "Provider='SQLNCLI10',DataSource='你的服务器,端口'"
- CONNECT USING ltr;
- if ltr.SQLCode <> 0 then
- MessageBox('连接失败', ltr.SQLErrText)
- return
- end if
- ll_id = 3
- ll_qty = 0
- ls_name = ''
- // 调用格式与普通函数一致:返回值即 RETURN 码,ref 参数即 OUTPUT 参数
- ll_rc = ltr.sp_s82_out(ll_id, ls_name, ll_qty)
- MessageBox('RPCFUNC 结果', '返回码=' + String(ll_rc) + ' name=' + ls_name + ' qty=' + String(ll_qty))
- DISCONNECT USING ltr;
- destroy ltr
复制代码
实测读数:返回码 300(30 的 10 倍,RETURN 值准确拿到)、name=C、qty=30(两个 OUTPUT 参数全部正确回填)。再调 ltr.sp_s82_ret(21),返回码 42,分毫不差。
两个实测细节。第一,OUTPUT 的 string 参数不需要预填空格占位——我预填了 10 个空格,回填后 Len 变成 1(就是 'C'),SNC 会按实际长度回传。第二,RPCFUNC 声明后必须在 CONNECT 之后调用,调用走的就是这个事务对象的连接;用 SQLCA 也行,只要把 SQLCA 的类型换成你的标准类(工程设置里可以指定默认事务对象类)。
八、RPCFUNC 的两个坑:不存在的过程与被丢弃的结果集
坑一:RPCFUNC 调不存在的数据库过程,不会崩,但也不会提醒你调对了。实测对 uo_trans_sp 里声明的一个不存在的过程发起调用:返回值 0、SQLCode=-1、SQLErrText=「Can not get parameters of procedure dbo.sp_s82_nosuch.」。注意返回值是 0 不是 -1——只看返回码会以为调用成功了,必须再看 SQLCode。这个错误发生在调用时(PB 向服务器要参数描述失败),编译期查不出来。
坑二:RPCFUNC 调带结果集的过程,结果集被静默丢弃。实测对 sp_s82_rs 发起 RPCFUNC 调用:返回码 0、SQLCode=0、SQLNRows=0,一切「正常」,但三行数据无处可取——RPCFUNC 只管参数和返回码,结果集不是它的输出通道。带结果集的过程老老实实走 DECLARE 路线。
九、事务归属:过程里的 DML 服从外层事务
AutoCommit=false 时,存储过程内部做的 UPDATE 算不算外层事务的一部分?实测 sp_s82_dml 把 qty 改成 999,然后 ROLLBACK:
- // 输入值在前段定义:目标行 id 与要写入的 qty
- long ll_id, ll_qty, ll_view
- ll_id = 2
- ll_qty = 999
- DECLARE c10 PROCEDURE FOR dbo.sp_s82_dml @id = :ll_id, @qty = :ll_qty;
- EXECUTE c10;
- // 同一连接内立即读:能读到 999(自己看自己的未提交数据)
- SELECT qty INTO :ll_view FROM dbo.t_s82 WHERE id = 2 USING SQLCA;
- ROLLBACK USING SQLCA;
- // 回滚后再读:回到 20,过程内的 UPDATE 被一并撤销
- SELECT qty INTO :ll_view FROM dbo.t_s82 WHERE id = 2 USING SQLCA;
- MessageBox('回滚后', 'qty=' + String(ll_view))
- CLOSE c10;
复制代码
实测:回滚前同连接读到 999,ROLLBACK 后读回 20。存储过程不是事务黑洞,它内部的 DML 完全服从外层事务对象的提交/回滚。前提是过程内部自己没有 COMMIT/ROLLBACK——如果过程里写了显式事务控制,边界就归它管了。
十、出错形态与出错后的连接
EXECUTE 一个不存在的过程,实测返回 SQLCode=-1,SQLErrText 是驱动原文:「SQLSTATE = 42000 Microsoft SQL Server Native Client 10.0 找不到存储过程 'dbo.sp_s82_nosuch'。」——三段式:ODBC 状态码、驱动名、服务器报文原文。
还没 EXECUTE 就 FETCH,实测返回 -1,错误文本是「Cursor is not open」,语义直白。
最关心的是出错之后连接还能不能用。实测在同一条连接上先 EXECUTE 失败,再对另一个正确声明的过程 EXECUTE + FETCH:第二次 EXECUTE 返回 0、FETCH 取回 3,C,30,完全正常。单次 EXECUTE 失败不毒化连接,可以放心走「试一下、错了读错误文本、继续干正事」的流程。
十一、SET NOCOUNT 要不要写
MSSQL 老手都知道「存储过程里务必 SET NOCOUNT ON」的祖训——否则 DML 的「(1 行受影响)」消息块会被当成结果集干扰客户端。那 PB 需要吗?我专门建了两个对照过程(一个 SET NOCOUNT ON、一个 OFF,都是 UPDATE + SELECT 结构)实测:
- NOCOUNT OFF 的混合过程:EXECUTE 返回 0,第一次 FETCH 直接落到 SELECT 的数据行(1,A,11),循环到 100 结束,读数干净;
- NOCOUNT ON 的 DML 过程:EXECUTE 返回 100,行为与 OFF 版完全一致。
结论:SNC 接口下 PB 的 FETCH 会自动跳过「n rows affected」状态块,NOCOUNT 不写也不会多出脏行。祖训对其它客户端(DB-Library、部分 ORM)成立,对 PB 嵌入式路线可以放宽。当然,过程是要给多种客户端用的,写了没坏处——只是 PB 侧别再把它当「必须」的坑来背。
十二、PB12.5 差异
- 嵌入式四步、RPCFUNC 声明与调用语法、各返回值语义:PB10 与 PB12.5 编译行为一致。本轮 16 个探针对象(含事务标准类)在 PB10 与 PB12.5 双轨下全量编译均 0 错误,PB10 侧对象齐全(17 个对象含应用对象)。
- PB12.5 连库实跑全部通过(15 个测试用例,含错误路径);PB10 侧因本机运行环境未装 SQL Server Native Client(SNC 接口报「is not supported in your current installation」),连库环节未做运行级验证,属环境限制而非版本差异——在你装了 SNC 的 PB10 环境里,本文语法不受版本影响。
- 一句话口径:PB10 基准 · PB12.5 实测;存储过程三路线均为 PB10 时代定型的特性,无版本差异。
十三、坑清单
- EXECUTE 成功返回 0,SQLNRows 恒为 0——别拿它判行数,行数靠 FETCH 循环自己数。
- DML 类过程 EXECUTE 后 SQLCode=100 不是 0——100 是「无结果集」不是错,-1 才是错。
- 嵌入式 OUTPUT 参数:编译过、执行成、值不回——要输出值只有 RPCFUNC。
- 嵌入式路线拿不到 RETURN 码——同理,返回码只有 RPCFUNC 能拿。
- RPCFUNC 调不存在的过程返回 0 不报 -1——必须再查 SQLCode 才能发现失败。
- RPCFUNC 会静默丢弃结果集——带结果集的过程走 DECLARE。
- 结果集耗尽的 100 之后继续 FETCH 会自动接续下一个结果集——多结果集过程不用重新 EXECUTE。
- 未 EXECUTE 先 FETCH 报「Cursor is not open」(-1)——顺序别乱。
- DECLARE 出来的语句句柄要用 CLOSE 收,同一事务对象里别攒一堆不关的声明。
- 过程内的 DML 服从外层事务——AutoCommit=false 下 ROLLBACK 能撤销过程内部的 UPDATE。
- OUTPUT 的 string 参数不需要预填空格,SNC 按实际长度回填。
- 单次 EXECUTE 出错不毒化连接,错误文本是驱动原文(SQLSTATE 42000 = 找不到过程)。
- SET NOCOUNT 对 PB 嵌入式路线非必须——FETCH 自动跳过状态块;给多客户端用的过程照写不误。
十四、小结
存储过程三路线分工可以一句话记住:取结果集用 DECLARE(FETCH 循环 + 自动跨结果集),要输出参数和返回码用 RPCFUNC(transaction 标准类),动态拼过程名的场景才轮到动态 SQL 第四形态。嵌入式路线那些「编译放行但不干活」的语法(OUTPUT、RETURN)是本文最大的坑——它们不报错,只会让你在运行期对着一成不变的变量值发呆。写完这段我自己的感受是:PB 对存储过程的支持比传说中扎实,但文档没告诉你「哪条路能拿到什么」,这一格一格的读数,就是替你踩出来的地图。 |