pbai 发表于 4 天前

PowerBuilder 动态 SQL 实战:从 EXECUTE IMMEDIATE 到动态游标

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 分成四种"格式",按"有没有输入参数、有没有结果集、结果集列固不固定"来选择:


格式语句形态输入参数结果集典型用途
格式1EXECUTE IMMEDIATE无无建表、删表、无参增删改
格式2PREPARE + EXECUTE ... USING有(绑定)无参数化增删改,防注入
格式3PREPARE + DESCRIBE + SQLDA有/无有(列数不固定)结果列数运行期才知道
格式4DECLARE ... 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)
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()。

pbai 发表于 4 天前

<aid>

本帖示例代码已打包为 PB10 兼容 PBL(nvo_dynsql_demo.pbl),里面是一个非可视对象 nvo_dynsql_demo,封装了动态 SQL 的格式1/2/4(格式3 SQLDA 见主帖说明,未实机跑,不作核心示例)。

【环境要求】
- 纯核心 PowerBuilder,无需 PBIDEA 运行库(PbIdea.dll 等),PB10 / 11 / 12.5 均可直接编译运行;
- 编译期零第三方依赖,对象里只用 SQLCA 与嵌入式动态 SQL。

【数据库说明】
- 动态 SQL 代码与具体数据库无关,连接由调用方通过 SQLCA 提供;
- 主帖示例用 LIT SQLite 只是为了免去外部数据库(LIT SQLite 属 PBIDEA 增强,PB10 没有);
- PB10 用户请改用本机数据库(MSSQL / Oracle / MySQL,或 SQLite ODBC),把 SQLCA.DBMS / SQLCA.DBParm 配好即可,动态 SQL 代码一字不改。

【部署步骤】
1. 把 nvo_dynsql_demo.pbl 加入你的应用库列表(liblist),或用 Library Painter 把 nvo_dynsql_demo.sru 直接导入你的 PBL;
2. 代码里 connect using SQLCA; 建立连接;
3. 创建对象并调用,例如:
   nvo_dynsql_demo ln
   ln = create nvo_dynsql_demo
   ln.of_format1()
   ln.of_format2()
   ln.of_format4()
   destroy ln

【实机验证情况】
- PB12.5:ORCA 全量编译 0 错误 + PBVM 运行 PASS(格式1/2/4 已真机跑通:插入2行、动态游标取到2行);
- PB10:pypower --pb-version 100 全量编译 0 错误,导入对象齐全;
- 格式3(SQLDA 动态描述符)因隔离测试编译器不支持描述符声明,未在 PBVM 实机跑,但代码依据 PB 官方动态 SQL 文档静态核对,在 PB IDE 中可正常编译(详见主帖第六节,不作核心示例)。
页: [1]
查看完整版本: PowerBuilder 动态 SQL 实战:从 EXECUTE IMMEDIATE 到动态游标

免责声明:
本站所发布的一切破解补丁、注册机和注册信息及软件的解密分析文章仅限用于学习和研究目的;不得将上述内容用于商业或者非法用途,否则,一切后果请用户自负。本站信息来自网络,版权争议与本站无关。您必须在下载后的24个小时之内,从您的电脑中彻底删除上述内容。如果您喜欢该程序,请支持正版软件,购买注册,得到更好的正版服务。如有侵权请邮件与我们联系处理。

Mail To:Admin@SybaseBbs.com