马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。
您需要 登录 才可以下载或查看,没有账号?站点注册
×
PowerBuilder 动态 SQL 全解:四种执行形态、SQLSA 与 SQLDA 的真实行为(PB10 基准 · PB12.5 实测)
阅读说明
1. 适用版本:基准 PB10,兼容 PB 12.5(不涉及 PBIDEA 组件,纯 PowerBuilder 内置能力)
2. 支持数据库:实测环境为 SQL Server 2008 R2(SNC 接口,Provider=SQLNCLI10);动态 SQL 语法本身不绑定 DBMS,Oracle / SQL Anywhere 等同样适用,但文中读数以 SQL Server 为准
3. 操作系统与环境要求:Windows 7+;需要能连上数据库的测试环境(运行级验证需 SNC 接口库)
4. 难度系数:★★★☆☆
5. 其它阅读说明:建议先读《嵌入式 SQL 实战》(静态 SQL 与游标基础)与《SyntaxFromSQL 全解》(本文的姊妹篇,讲动态生成 DataWindow,与本文分工不重叠);示例表建在 master 库,照抄即可跑
嵌入式静态 SQL 把语句写死在源码里,表名、列名、WHERE 条件都是编译期常量。可真实业务里总有"表名要运行时才知道""查询条件用户说了算"的时刻,这就轮到动态 SQL 登场。PowerBuilder 的动态 SQL 有四种官方形态,覆盖从"随手执行一句 DDL"到"结果集长什么样都不知道"的全部场景。
这篇文章把四种形态逐个跑了一遍,所有读数都在 SQL Server 2008 R2 + SNC 接口的真实环境里测得,重点记录那些文档不写、一踩就疼的行为细节。
一、什么时候用动态 SQL,什么时候不用
静态 SQL(SELECT ... INTO、INSERT、DECLARE 游标)编译期就检查语法、绑定变量,运行快、错误早暴露,能用就用。动态 SQL 的用武之地只有三类:
- 对象名在编译期不存在:按月分表(订单_202610)、按租户分库,表名要拼出来。
- 语句结构由用户或配置决定:自定义查询器、报表条件构造器。
- DDL 操作:建表、改列、建索引,静态 SQL 根本写不了。
如果你只是 WHERE 条件里的值在变,用静态 SQL 的绑定变量就够了,别上动态 SQL——那是给自己找事。
二、四种形态总览
| 形态 | 关键语句 | 输入参数 | 结果集 | 典型用途 | | 一 | EXECUTE IMMEDIATE | 无 | 无 | DDL、无参 DML | | 二 | PREPARE + EXECUTE SQLSA USING | 有(? 占位) | 无 | 带参 DELETE / UPDATE / INSERT | | 三 | DECLARE 动态游标 + PREPARE | 有(? 占位) | 有,列已知 | 列结构已知的动态查询 | | 四 | DECLARE + PREPARE + DESCRIBE | 有(描述符) | 有,列未知 | 任意 SELECT 的通用执行器 |
记忆方法:结果集"有没有"决定用不用游标,参数"知不知道"决定要不要 DESCRIBE。形态一和形态二不返回结果集;形态三和形态四的区别只是"结果集长什么样编译期知不知道"。
三、形态一:EXECUTE IMMEDIATE
语法只有一行:
- string ls_sql
- // 输入值:要执行的建表语句(字符串变量前段定义并赋值)
- ls_sql = "create table dbo.t_tmp(id int primary key, name nvarchar(10))"
- EXECUTE IMMEDIATE :ls_sql USING SQLCA;
- // 结尾输出结果
- MessageBox('执行结果', 'SQLCode=' + String(SQLCA.SQLCode))
复制代码
语句可以放在字符串变量里(变量前加冒号),也可以直接写字面量。USING SQLCA 可以省略,默认就是 SQLCA。
实测读数(AutoCommit=false 连接,表 dbo.t_tmp 由它亲手创建):
| 步骤 | SQLCode | 备注 | | EXECUTE IMMEDIATE 建表 | 0 | DDL 直接执行 | | EXECUTE IMMEDIATE 插入 2 行 | 0 | SQLNRows = 1(每条一条) | | ROLLBACK 之后查这张表 | -1 | 报 42S02「对象名无效」,表没了 |
最重要的一条结论就在最后一行:形态一不自动提交,它参与当前事务。很多老资料说"EXECUTE IMMEDIATE 执行完就提交了",在 SNC 接口下不成立——ROLLBACK 连刚建的表一起回滚掉,DDL 也不例外。如果你不想让它卷进事务,把事务对象的 AutoCommit 设为 true,或者执行完立刻 COMMIT。
其余实测行为,每条都有探针背书:
- 拿它执行 SELECT 不报错:SQLCode 返回 0,但结果集直接丢弃,一行数据都拿不到。想查数请用形态三/四,或者老老实实静态 SQL。
- 空字符串直接拒绝:EXECUTE IMMEDIATE :ls_empty 返回 -1,SQLErrText 是「Syntax Error: No SQL Command」——这是 PB 客户端层拦下的,SQLDBCode 为 0,根本没到数据库。
- 非法 SQL 报驱动原文:查一张不存在的表,SQLCode=-1、SQLDBCode=208、SQLErrText 是「SQLSTATE = 42S02 对象名无效」的完整三行原文,判错直接看这三个属性。
- 出错之后连接还能用:报错的下一条语句正常执行,不需要重连。
四、形态二:PREPARE + EXECUTE SQLSA USING
语句里有参数值要填时用这形态。注意它的正解没有 DECLARE,PREPARE 之后直接 EXECUTE SQLSA:
- long la, lb, lc
- string ls_name
- // 输入值:主键 3 要删除,7/G/20 要插入(变量前段定义并赋值)
- la = 3
- // 删除指定行
- PREPARE SQLSA FROM "delete from dbo.t_tmp where id = ?" USING SQLCA;
- EXECUTE SQLSA USING :la;
- // SQLNRows 就是受影响行数
- MessageBox('删除', '受影响 ' + String(SQLCA.SQLNRows) + ' 行')
- // 多参数按 ? 顺序对应
- PREPARE SQLSA FROM "insert into dbo.t_tmp values(?, ?, ?)" USING SQLCA;
- la = 7
- ls_name = 'G'
- lb = 20
- lc = 30
- EXECUTE SQLSA USING :la, :ls_name, :lb;
- MessageBox('插入', 'SQLCode=' + String(SQLCA.SQLCode))
复制代码
实测确认的行为:
- SQLNRows 如实反映受影响行数:DELETE 删 1 行报 1,INSERT 插 1 行报 1。
- 参数按 ? 出现顺序对应 USING 列表,类型按 PB 变量自动映射,long 与 string 混用没问题。
- 同一 SQLSA 可以反复 PREPARE:上一条执行完,直接对 SQLSA PREPARE 下一条语句即可,不需要(也没办法)"释放"它。
- 最坑的一条:PREPARE 不检查 SQL 是否合法。把表名写成不存在的,PREPARE 照样返回 0;要等 EXECUTE 才报 -1。所以执行完必须查 SQLCode,只看 PREPARE 的返回值会以为万事大吉。这个行为在形态三的游标上同样存在,见下一节。
五、形态三:动态游标(结果集列已知)
查询语句是动态拼的,但 SELECT 出来几列、什么类型编译期都知道,用动态游标:
- long ll_id, la
- string ls_name
- // 输入值:取 id 大于 0 的全部行(变量前段定义并赋值)
- la = 0
- // DECLARE 是声明性语句,放在 PREPARE 之前即可
- DECLARE c_tmp DYNAMIC CURSOR FOR SQLSA;
- PREPARE SQLSA FROM "select id, name from dbo.t_tmp where id > ?" USING SQLCA;
- OPEN DYNAMIC c_tmp USING :la;
- FETCH c_tmp INTO :ll_id, :ls_name;
- do while SQLCA.SQLCode = 0
- // 逐行处理
- MessageBox('取数', String(ll_id) + ' = ' + ls_name)
- FETCH c_tmp INTO :ll_id, :ls_name;
- loop
- CLOSE c_tmp;
复制代码
实测确认的行为:
- 无参数时 OPEN 不带 USING:PREPARE 的语句里没有 ?,OPEN DYNAMIC c_tmp 就行,带空参数反而画蛇添足。
- FETCH INTO 有隐式类型转换:int 列的值 FETCH 进 string 变量,拿到的是 '30' 这样的字符串,不报错。这既是方便也是隐患——列类型对错了不会有人提醒你。
- 取完的标志是 SQLCode = 100,不是 SQLNRows 也不是别的。
- CLOSE 一个没执行成功(或没结果集)的游标,报错文案是「Procedure has not been executed or has no results」——说的是 Procedure 不是 Cursor,第一次见多半会愣一下,其实指的就是你那个动态游标。
- PREPARE 一条非法语句,游标照样 OPEN 成功,错误要等 FETCH 才爆出来。理由同形态二的第 4 条:PREPARE 阶段不做合法性检查。
六、形态四:DESCRIBE 与 SQLDA(结果集列未知)
连"SELECT 出来几列"都要运行时才知道,就要请出动态描述区 SQLDA(DynamicDescriptionArea)。它和 SQLSA 一样是系统预定义的全局变量,拿来就用:
- long ll_row
- string ls_name
- // 输入值:任意一条 SELECT(演示用固定语句,实战可由用户构造)
- PREPARE SQLSA FROM "select id, name from dbo.t_tmp" USING SQLCA;
- // DESCRIBE 把结果集结构填进 SQLDA
- DESCRIBE SQLSA INTO SQLDA;
- // 此时 SQLDA.NumOutputs 是列数,OutParmType 数组是每列的类型
- OPEN DYNAMIC c_tmp USING DESCRIPTOR SQLDA;
- FETCH c_tmp USING DESCRIPTOR SQLDA;
- do while SQLCA.SQLCode = 0
- ls_name = SQLDA.GetDynamicString(2)
- // 结尾输出结果
- MessageBox('第一列', 'name=' + ls_name)
- FETCH c_tmp USING DESCRIPTOR SQLDA;
- loop
- CLOSE c_tmp;
复制代码
注意 DECLARE 也要写,且必须 FOR SQLSA(完整代码见第十一节)。
带输入参数时,在 OPEN 之前用 SetDynamicParm 按序号塞值:
- long la
- // 输入值:id 大于 1(变量前段定义并赋值)
- la = 1
- PREPARE SQLSA FROM "select id, name from dbo.t_tmp where id > ?" USING SQLCA;
- DESCRIBE SQLSA INTO SQLDA;
- // 第 1 个 ? 塞入 la 的值
- SQLDA.SetDynamicParm(1, la)
- OPEN DYNAMIC c_tmp USING DESCRIPTOR SQLDA;
复制代码
实测确认的行为,这一节坑最密:
- DESCRIBE 之前 NumInputs、NumOutputs 恒为 0,DESCRIBE 之后才有值。实测一条两列、一个 ? 的语句:NumInputs=1、NumOutputs=2。
- OutParmType 按"数据库类型映射 PB 类型":int 列映射成 TypeLong!(不是 TypeInteger!),nvarchar 列映射成 TypeString!。
- InParmType 却不按这个映射:对 int 列上的 ?,实测 InParmType 第 1 个元素既不是 TypeLong! 也不是 TypeInteger!,而是其它类型。输入参数的描述类型别拿来当列类型用。
- GetDynamicString 读数值列返回空串。这是形态四和形态三最大的行为差异:形态三的 FETCH INTO 会隐式转换(int 进 string 变量拿 '30'),形态四的 GetDynamicString 拿数值列只能拿到空串。正确姿势是先看 OutParmType 再决定调 GetDynamicString 还是 GetDynamicNumber。
- SetDynamicParm 索引越界静默成功:语句只有 1 个输入参数,SetDynamicParm(2, 99) 照样返回成功,SQLCode 也是 0,没有任何报警。参数个数只能自己把关。
- 越界读的行为按索引分岔,最危险的是 0:实测 3 列的结果集,GetDynamicString(4) 返回空串、GetDynamicNumber(9) 返回 0,都静默;GetDynamicString(0) 直接把进程干崩(0xC0000005,TRY-CATCH 兜不住)。索引必须从 1 开始、绝不传 0,这是本文全篇最重的一条。
七、动态存储过程:能做什么,不能做什么
语法上还存在 DECLARE 动态过程:
- // DECLARE 动态过程 + EXECUTE 后 FETCH:实测可跑通
- DECLARE p_tmp DYNAMIC PROCEDURE FOR SQLSA;
- PREPARE SQLSA FROM "select id, name from dbo.t_tmp where id >= 2" USING SQLCA;
- EXECUTE p_tmp;
- FETCH p_tmp INTO :ll_id, :ls_name;
- CLOSE p_tmp;
复制代码
实测结论:
- PREPARE 一条 SELECT、用 DECLARE 动态过程执行并 FETCH,在 SQL Server 下能取到数据(实测取到 id=2 那行)。存储过程路线和普通查询路线在 SNC 接口下是打通的。
- EXECUTE 动态过程 USING :变量 编译不过:报 C0015「Undefined variable」。帮助文档里旧式写法用 DECLARE 过程带参数执行,但 PB12.5 编译器不认这写法——以编译器为准,带参执行请走形态二(PREPARE + EXECUTE SQLSA USING),过程调用可 PREPARE 一句 execute dbo.p_xxx ? 再用动态游标包住。
- PREPARE execute dbo.p_xxx ? 同样不做存在性检查:过程不存在 PREPARE 也返回 0,FETCH 时才报 -1。
- 顺带一提,连接都没建立时就去 PREPARE,会直接吃一个运行期错误「Database command not successfully prepared」——所以动态 SQL 前先确认 CONNECT 成功,这不是可选的好习惯,是必需的。
八、DECLARE 的三条规则
- DECLARE 是声明性语句,不是可执行语句——它不产生运行动作,可以出现在 PREPARE 之前,也可以按帮助文档的说法声明成全局的。
- 游标与过程都必须 FOR SQLSA。写成 DECLARE c1 DYNAMIC CURSOR FOR SQLDA 编译直接报 C0103「Dynamic SQL command must reference DynamicStagingArea object」——SQLDA 是描述区不是暂存区,不能接 DECLARE。这是本文实测中唯一一处编译期报错的写法错误。
- 游标名/过程名的作用域按 PB 变量规则来,局部声明只在当前函数内有效。
九、PB12.5 差异
本文全部代码在 PB10 与 PB12.5 双轨验证:两轨编译均 0 错误,语法层面零差异。运行级验证受本机 PB10 运行环境限制(无 SNC 接口库,无法连 SQL Server),运行级行为的读数全部来自 PB12.5 连库实测;PB10 侧完成了编译级验证。动态 SQL 是 PB10 时代就定型的老特性,四个形态的语法在两版间没有任何增删。
十、坑清单
- 形态一不自动提交,AutoCommit=false 下 ROLLBACK 连 CREATE TABLE 一起回滚。
- EXECUTE IMMEDIATE 执行 SELECT:不报错,但结果集直接丢弃。
- 空串直接 -1:「Syntax Error: No SQL Command」,PB 客户端层拦截。
- PREPARE 不校验 SQL 合法性,表名写错它也返回 0;错误要等 EXECUTE / FETCH 才爆。
- 同一 SQLSA 可以反复 PREPARE 复用,换语句不用换暂存区。
- FETCH INTO 隐式转换:int 列 FETCH 进 string 变量拿到字符串。
- CLOSE 未执行的游标报「Procedure has not been executed or has no results」,文案与游标无关,别慌。
- DECLARE 必须写 FOR SQLSA,写 FOR SQLDA 编译报 C0103。
- DESCRIBE 之前 NumInputs / NumOutputs 恒为 0。
- OutParmType 是列类型映射:int 到 TypeLong!、nvarchar 到 TypeString!。
- InParmType 不按列类型映射,实测对 int 列的 ? 两者都不是,别拿来当列类型判断。
- GetDynamicString 读数值列返回空串,必须按 OutParmType 分派 GetDynamicString / GetDynamicNumber。
- SetDynamicParm 越界静默成功;GetDynamicString 超界返空、GetDynamicNumber 超界返 0;GetDynamicString(0) 直接崩进程(0xC0000005)。
- EXECUTE 动态过程 USING :变量 编译报 C0015;动态过程不带参数可 EXECUTE 后直接 FETCH 取结果集。
- 连接未建立就 PREPARE,运行期报「Database command not successfully prepared」。
十一、完整一条龙示例
四种形态串成一条龙,建表、回滚验证、带参删除、动态游标、描述符取数,全部实测通过,输出为一段拼好的读数串:
- // 四种形态一条龙演示(SQL Server 2008 R2 + SNC 实测读数见注释)
- string ls_out, ls_name
- long ll_n, la, ll_id
- // ① 连接(AutoCommit=false,专测形态一的事务归属)
- SQLCA.DBMS = 'SNC SQL Native Client(OLE DB)'
- SQLCA.LogId = 'sa'
- SQLCA.LogPass = '你的口令'
- SQLCA.ServerName = '你的服务器,端口'
- SQLCA.Database = 'master'
- SQLCA.AutoCommit = false
- SQLCA.DBParm = "Provider='SQLNCLI10',DataSource='你的服务器,端口'"
- CONNECT USING SQLCA;
- if SQLCA.SQLCode <> 0 then
- MessageBox('连接失败', SQLCA.SQLErrText)
- return
- end if
- ls_out = 'F1'
- // ② 形态一:建表 + 插入(不提交)
- EXECUTE IMMEDIATE "create table dbo.t_demo78(id int primary key, name nvarchar(10))" USING SQLCA;
- EXECUTE IMMEDIATE "insert into dbo.t_demo78 values(1,'A')" USING SQLCA;
- EXECUTE IMMEDIATE "insert into dbo.t_demo78 values(2,'B')" USING SQLCA;
- // ③ ROLLBACK 验证:表会被一起回滚掉
- ROLLBACK USING SQLCA;
- select count(*) into :ll_n from dbo.t_demo78 using SQLCA;
- if SQLCA.SQLCode <> 0 then
- ls_out = ls_out + ' 回滚连表一起没了=YES'
- else
- ls_out = ls_out + ' 回滚后还在 CNT=' + String(ll_n)
- end if
- // ④ 重建并提交,供后续形态使用
- EXECUTE IMMEDIATE "create table dbo.t_demo78(id int primary key, name nvarchar(10))" USING SQLCA;
- EXECUTE IMMEDIATE "insert into dbo.t_demo78 values(1,'A')" USING SQLCA;
- EXECUTE IMMEDIATE "insert into dbo.t_demo78 values(2,'B')" USING SQLCA;
- EXECUTE IMMEDIATE "insert into dbo.t_demo78 values(3,'C')" USING SQLCA;
- COMMIT USING SQLCA;
- ls_out = ls_out + ' F2'
- // ⑤ 形态二:带参 DELETE
- PREPARE SQLSA FROM "delete from dbo.t_demo78 where id = ?" USING SQLCA;
- la = 3
- EXECUTE SQLSA USING :la;
- ls_out = ls_out + ' DEL=' + String(SQLCA.SQLNRows)
- select count(*) into :ll_n from dbo.t_demo78 using SQLCA;
- ls_out = ls_out + ' CNT=' + String(ll_n) + ' F3'
- // ⑥ 形态三:动态游标
- DECLARE c_d78 DYNAMIC CURSOR FOR SQLSA;
- PREPARE SQLSA FROM "select id, name from dbo.t_demo78 where id > ?" USING SQLCA;
- la = 0
- OPEN DYNAMIC c_d78 USING :la;
- FETCH c_d78 INTO :ll_id, :ls_name;
- do while SQLCA.SQLCode = 0
- ls_out = ls_out + ' [' + String(ll_id) + ',' + ls_name + ']'
- FETCH c_d78 INTO :ll_id, :ls_name;
- loop
- CLOSE c_d78;
- ls_out = ls_out + ' F4'
- // ⑦ 形态四:DESCRIBE + SQLDA
- DECLARE c_d78b DYNAMIC CURSOR FOR SQLSA;
- PREPARE SQLSA FROM "select id, name from dbo.t_demo78" USING SQLCA;
- DESCRIBE SQLSA INTO SQLDA;
- ls_out = ls_out + ' NO=' + String(SQLDA.NumOutputs)
- OPEN DYNAMIC c_d78b USING DESCRIPTOR SQLDA;
- FETCH c_d78b USING DESCRIPTOR SQLDA;
- do while SQLCA.SQLCode = 0
- ls_name = SQLDA.GetDynamicString(2)
- ls_out = ls_out + ' [' + ls_name + ']'
- FETCH c_d78b USING DESCRIPTOR SQLDA;
- loop
- CLOSE c_d78b;
- DISCONNECT USING SQLCA;
- // 结尾输出结果
- MessageBox('动态 SQL 一条龙', ls_out)
复制代码
实测输出(PB12.5 连库运行):
- F1 回滚连表一起没了=YES F2 DEL=1 CNT=2 F3 [1,A] [2,B] F4 NO=2 [A] [B]
复制代码
每一段都与上文各节的单独探针读数一致。
形态四里故意用 GetDynamicString(2) 只取 name 列,就是为了绕开第 12 条坑:id 是数值列,GetDynamicString 读它只会拿到空串。读懂这条,形态四才算真会用。 |