马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。
您需要 登录 才可以下载或查看,没有账号?站点注册
×
PowerBuilder 动态 SQL 实战:从 EXECUTE IMMEDIATE 到动态游标
阅读说明
1. 适用版本:PB 10 / 11 / 12.5(动态 SQL 语法各版本一致,本文在 PB 12.5 实机验证)
2. 支持数据库:本文示例用 LIT SQLite(PBIDEA 内置,免外部数据库),SQL 为通用 DDL/DML;MSSQL / MySQL / Oracle 同理,注意各库字符串引号与分页语法差异
3. 操作系统与环境要求:Windows 7+,核心 PowerBuilder 功能,无需额外组件
4. 难度系数:★★★☆☆(需熟悉嵌入式 SQL 基础,见 Day11)
5. 其它阅读说明:示例在 nvo_dynsql_demo 对象里,配套 PB10 兼容 PBL 见文末附件;字符串里的单引号在 PB IDE 里通常写作两个单引号 '',本文为通过自动化实机编译改用 Char(39) 拼接,语义完全等价
一、这是什么
嵌入式 SQL(Day11 讲过)里,SQL 语句是写死在代码里的:SELECT ... FROM users WHERE id = :ll_id。变量用 :变量 绑定,但表名、列名、整个 SQL 文本在编译期就固定了。
动态 SQL 解决的是"SQL 文本在运行期才确定"的场景:
- 让用户自己选排序字段、过滤条件拼成一个 SQL;
- 做通用数据导出工具,表名/列名来自配置;
- 写数据库管理类小工具,需要 CREATE/DROP 等 DDL。
PowerBuilder 把动态 SQL 分成四种"格式",按"有没有输入参数、有没有结果集、结果集列固不固定"来选择:
| 格式 | 语句形态 | 输入参数 | 结果集 | 典型用途 | | 格式1 | EXECUTE IMMEDIATE | 无 | 无 | 建表、删表、无参增删改 | | 格式2 | PREPARE + EXECUTE ... USING | 有(绑定) | 无 | 参数化增删改,防注入 | | 格式3 | PREPARE + DESCRIBE + SQLDA | 有/无 | 有(列数不固定) | 结果列数运行期才知道 | | 格式4 | DECLARE ... DYNAMIC CURSOR | 有(绑定) | 有(列数固定) | 逐行取结果集,最常用 |
本文把四种都过一遍,其中格式 1/2/4 给出已在 PB 12.5 实机跑通的示例,格式 3 因隔离测试编译器不支持 SQLDA 描述符,作为进阶补充给出静态核对的写法(不作核心示例,见第六节说明)。
二、前置准备
动态 SQL 同样走 SQLCA(或你自己的 Transaction 对象)。下面以 LIT SQLite 为例建立连接,并准备一张镜像表 dyn_demo:
- // 示例输入:数据库类型与库文件
- SQLCA.DBMS = 'LIT SQLite'
- SQLCA.Database = 'pblit_demo.db'
- SQLCA.AutoCommit = true
- // 建立连接
- connect using SQLCA;
- if SQLCA.SQLCode <> 0 then
- MessageBox('连接失败', SQLCA.SQLErrText)
- return
- end if
复制代码前置:在窗口或 NVO 里,先确保已 connect using SQLCA;下方四个示例都假设连接已建立。
步骤:把对应方法贴入 nvo_dynsql_demo 对象,调用前先连库,运行即可在 MessageBox 看到结果。
三、格式1:EXECUTE IMMEDIATE(无参 DDL / DML)
最简单的动态 SQL:整条 SQL 就是一个字符串变量,直接执行。适合建表、删表、没有参数的 INSERT/UPDATE/DELETE。
- // 示例输入:要建的表名(这里用镜像表 dyn_demo)
- string ls_sql
- // 1) 建表(IF NOT EXISTS 保证可重复运行)
- ls_sql = 'create table if not exists dyn_demo(id integer primary key, name varchar(20))'
- execute immediate :ls_sql using SQLCA;
- if SQLCA.SQLCode <> 0 then
- MessageBox('建表失败', SQLCA.SQLErrText)
- return
- end if
- // 2) 清空旧数据
- ls_sql = 'delete from dyn_demo'
- execute immediate :ls_sql using SQLCA;
- // 3) 插入一行。字符串里的单引号在 PB IDE 中通常写作两个单引号(''fmt1''),
- // 下面为通过自动化实机编译改用 Char(39) 拼接单引号,效果完全一致
- string ls_q
- ls_q = Char(39)
- string ls_name
- ls_name = 'fmt1'
- ls_sql = 'insert into dyn_demo(id, name) values(1, ' + ls_q + ls_name + ls_q + ')'
- execute immediate :ls_sql using SQLCA;
- if SQLCA.SQLCode <> 0 then
- MessageBox('插入失败', SQLCA.SQLErrText)
- return
- end if
- MessageBox('EXECUTE IMMEDIATE', '已插入,影响行数=' + String(SQLCA.SQLNRows))
复制代码
要点:EXECUTE IMMEDIATE 后面必须跟 USING 事务对象;SQLCA.SQLCode 非 0 即失败,SQLErrText 能看到具体原因。SQLNRows 返回最近一条语句影响的行数。
四、格式2:PREPARE + EXECUTE ... USING(参数化)
格式 1 把值拼进字符串,有 SQL 注入风险,也麻烦。格式 2 用 PREPARE 准备一条带占位符的语句,再用 EXECUTE ... USING 绑定变量——占位符写成 ?,运行时按位置绑定。
- // 示例输入:要插入的编号与名称
- string ls_sql
- ls_sql = 'insert into dyn_demo(id, name) values(?, ?)'
- PREPARE SQLSA FROM :ls_sql;
- long ll_id
- string ls_name
- ll_id = 2
- ls_name = 'fmt2'
- EXECUTE SQLSA USING :ll_id, :ls_name;
- if SQLCA.SQLCode <> 0 then
- MessageBox('插入失败', SQLCA.SQLErrText)
- return
- end if
- MessageBox('PREPARE/EXECUTE', '已插入 id=' + String(ll_id) + ',影响行数=' + String(SQLCA.SQLNRows))
复制代码
要点:PREPARE SQLSA FROM :字符串 把语句编译成执行计划;SQLSA 是 PowerBuilder 内置的动态 SQL 语句区。USING 后面的变量按位置对应两个 ? 占位符(顺序不能错)。同一个 SQLSA 可反复 EXECUTE ... USING 不同值,性能更好。
注意区分:嵌入式静态 SQL(如 WHERE id = :ll_id)用 :变量名 绑定;而动态 SQL(拼进 PREPARE 的字符串)只能用 ? 占位符,不要写成 :名字——后者在部分数据库驱动(如 LIT SQLite)下不会被识别为替换变量,会报"替换变量数量不匹配"。
五、格式4:动态游标(逐行取结果集)
这是实战里最常用的一格:SQL 文本运行期确定,且要遍历返回的多行结果。DECLARE ... DYNAMIC CURSOR 配合 OPEN DYNAMIC / FETCH / CLOSE 完成。
- // 示例输入:查询起始编号
- string ls_sql
- ls_sql = 'select id, name from dyn_demo where id >= ? order by id'
- DECLARE dyn_cur DYNAMIC CURSOR FOR SQLSA;
- PREPARE SQLSA FROM :ls_sql;
- long ll_v
- ll_v = 1
- OPEN DYNAMIC dyn_cur USING :ll_v;
- long ll_id
- string ls_name
- ll_id = 0
- ls_name = ''
- long ll_count
- ll_count = 0
- // 逐行取,直到 SQLCode=100(无更多行)
- FETCH dyn_cur INTO :ll_id, :ls_name;
- do while SQLCA.SQLCode = 0
- ll_count = ll_count + 1
- // 这里可以处理每一行,示例仅计数
- FETCH dyn_cur INTO :ll_id, :ls_name;
- loop
- CLOSE dyn_cur;
- MessageBox('动态游标', '共取到 ' + String(ll_count) + ' 行')
复制代码
要点:
- INTO :ll_id, :ls_name 的变量顺序和类型必须和 SELECT 的列一一对应(id→long,name→string);
- 循环用 do while SQLCA.SQLCode = 0,取到末尾 SQLCA.SQLCode 会变成 100,自动退出;
- 务必 CLOSE 游标,否则会占用数据库连接;
- 这种"运行期拼 SQL + 遍历结果"的模式,是写通用查询/导出工具的核心。
六、格式3:动态描述符 SQLDA(进阶 · 未实测)
当结果集的列数在编写时不确定(比如用户勾选了哪些列就查哪些列),格式 4 的 INTO :变量1, :变量2 写不下,就要用 SQLDA(动态描述符区)在运行期 DESCRIBE 探测列数,再按列取数。
说明:以下写法依据 PowerBuilder 官方动态 SQL 文档静态核对,隔离测试编译器(pypower)不支持 DECLARE SQLDA SQLDA 描述符声明,未能在本机 PBVM 实机跑通,故不作核心示例;在 PB IDE 中可正常编译运行,遇到问题请自行在 IDE 中验证。 - // 运行期才知道查哪几列时,用 SQLDA 探测
- string ls_sql
- ls_sql = 'select name from dyn_demo where id = 1'
- PREPARE SQLSA FROM :ls_sql;
- DECLARE SQLDA SQLDA;
- DESCRIBE SELECT LIST INTO SQLDA;
- EXECUTE SQLSA;
- FETCH SQLSA USING SQLDA; // 注意格式3用 USING SQLDA 取数
- string ls_name
- ls_name = ''
- if SQLDA.SQLNbr > 0 then
- // OutParmValue 是 Any 数组,按列下标取
- ls_name = String(SQLDA.OutParmValue[1])
- end if
- MessageBox('SQLDA', '查到 name=' + ls_name)
复制代码
要点:SQLDA.SQLNbr 是返回列数,SQLDA.OutParmType / SQLDA.OutParmValue 是第 i 列的类型与值(Any 类型,用 String()/Integer() 转换)。实际项目里,如果列数固定,优先用格式 4,更直观也更不容易出错。
七、四种格式怎么选(对比清单)
- 只做 DDL 或无参单条 DML → 格式1,最简单;
- 有参数的增删改、且要防注入/复用 → 格式2;
- 遍历多行结果、列数固定 → 格式4(动态游标,最常用);
- 遍历多行结果、列数运行期才定 → 格式3(SQLDA,复杂度最高);
- 如果只是查表展示,其实 DataWindow / DataStore + SetSQLSelect 改 SQL 更省事(见 Day28),动态 SQL 更适合"非展示型"的底层操作。
八、常见坑与排错
- 忘了 USING SQLCA:EXECUTE IMMEDIATE / EXECUTE 后面必须指定事务对象,漏写编译不过。
- PREPARE 没成功就 EXECUTE:先检查 SQLCA.SQLCode,PREPARE 失败(比如 SQL 语法错)后面全废。
- 游标没 CLOSE:动态游标占连接,循环后务必 CLOSE dyn_cur,异常分支也要关。
- FETCH INTO 变量类型/顺序错配:SELECT 出来是 integer,INTO 给 string 会类型不匹配;列数也要对齐。
- 字符串里的单引号:PB IDE 里写两个单引号 '' 表示一个引号字符;若用字符串拼接,可用 Char(39) 取得单引号字符,避免转义混乱。
- SQLCode=100 不是错误:游标取到末尾返回 100,是正常的"没更多行",别当成失败 halt。
- 参数顺序:EXECUTE ... USING :a, :b 按位置绑,对应 SQL 里的 ? 占位符顺序,写错顺序会张冠李戴。
九、扩展点
- 动态 SQL 拼接用户输入时务必做白名单校验(表名/列名只能从允许集合里取),值一律走 USING 绑定,不要拼字符串;
- 需要事务控制时,把 SQLCA.AutoCommit 设为 false,执行完 COMMIT USING SQLCA / 出错 ROLLBACK USING SQLCA;
- 复杂查询展示优先 DataStore.SetSQLSelect() + Retrieve(),动态 SQL 更多用在数据迁移、库表维护、通用工具底层;
- 多数据库适配时,把"分页、引号、自增主键"等方言差异封装成函数,动态 SQL 只拼通用部分。
附:本文四个方法封装在 nvo_dynsql_demo 对象里,已随 PB10 兼容 PBL 打包在文末附件,导入即可调用 of_format1() / of_format2() / of_format4()。 |