马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。
您需要 登录 才可以下载或查看,没有账号?站点注册
×
PBIDEA:用 uo_database 与 uo_database_pool 做数据库连接与连接池复用
阅读说明
1. 适用版本:PowerBuilder 10(PBIDEA 版本源 UPDATE\10 为基准);PB12.5 已实测同源码、同行为,代码可直接复用无需改写。
2. 支持数据库:SQL Server 2008 R2(本文全部示例的实测环境)。uo_database 另支持 ODBC / Oracle / MySQL / PostgreSQL / SQLite / DB2 / Sybase / SQLAnywhere / Firebird(InterBase) / SQLBase,连法相同,仅 DBMS 取值不同。
3. 操作系统与环境要求:Windows 7+;必须部署与 PB 版本匹配的 PbIdea.dll / Pbidea_cs.dll;工程库列表需同时挂载 sql.pbl 与 websuite.pbl;示例连接 127.0.0.1,18433(SQL Server 2008 R2 SP3,10.50.6000.34)。
4. 难度系数:★★★☆☆
5. 其它阅读说明:需先了解 transaction 对象的常用属性(DBMS/ServerName/Database/LogId/LogPass/AutoCommit/SQLCode/SQLErrText);示例会在 tempdb 建演示表 dbo.t_pbidea_demo,跑完自动删除,不影响现有数据。
一、为什么不要只用裸 SQLCA
写 PB 的人对 SQLCA 太熟了:一个全局的 transaction,CONNECT USING SQLCA; 一把梭。但真做项目会撞上四堵墙:
| 问题 | 裸 SQLCA 的表现 | | 多库 | SQLCA 是全局单例,要连第二个库就得再声明 transaction,连接参数散落各处 | | 复用 | 每次要连都得重新赋一遍 DBMS/LogId/LogPass,或写死在 ini 里再手工解析 | | 参数化 | 只能拼字符串,或者退回 PB 原生动态 SQL 的 PREPARE ... USING 四步式 | | 连接池 | 完全没有,多线程/多请求场景只能自己造轮子 |
PBIDEA 的 uo_database(在 sql.pbl)就是冲着这四点来的。它继承自 transaction,所以你熟的那套属性一个不少,但上面挂了一整套外部函数方法,连接方式、参数化取数、事务、连接池全都有。
二、uo_database 是什么
关键一句话:uo_database 是 transaction 的子类,不是普通 NVO。
- global type uo_database from transaction
复制代码
所以它自带 DBMS / Database / LogId / LogPass / ServerName / DBParm / AutoCommit / SQLCode / SQLErrText / SQLNRows 全套属性,用法和 SQLCA 一模一样,零学习成本。
生命周期
- constructor 自动调用 sqlCreate(),destructor 自动调用 sqlDestroy();
- 所以是 create 即用、destroy 即释放,不需要手工初始化。
方法全表(对照 sql.pbl 源码实证)
| 方法 | 返回 | 说明 | | Open() | boolean | 连接(需先设好属性) | | Open(string configFile) | boolean | 按配置文件连接 | | IsOpen() | boolean | 是否已连接 | | IsAlive() | boolean | 连接是否存活(探活用) | | SetTransLevel(int level) | — | 事务隔离级别 0–4 | | Close() | — | 断开 | | CommitTrans() / RollbackTrans() | — | 提交 / 回滚 | | ExecuteSQL(string sql, ...) | uo_recordset | 执行 SQL,变参(可散参或 any 数组) | | PrepareSQL(string sql) | uo_recordset | 预处理语句 | | ExecuteScaler(string sql) | any | 标量查询,只要一个值就用它 | | ExecuteScaler(sql, ref boolean success) | any | 带成功标志 | | RowsAffected() | long | 受影响行数 | | SaveConfig(string configFile) | int | 把当前属性存成配置文件 | | Printf(string fmt, ...) | int | 打印到控制台 |
事件:beginconnect() / endconnect(boolean ab_connect) / ondisconnect()。
DBMS 取值(源码注释原文)
"ODBC"、"MSS"(或 "Microsoft SQL Server" / "MSS Microsoft SQL Server")、"Oracle" / "O90" / "O12"(大写 O 打头皆视为 Oracle,需设 ThreadSafe="Yes")、"InterBase" / "Firebird"、"SQLBase"、"DB2"、"Sybase"、"MySQL"、"PostgreSQL"、"SQLite"、"SQLAnywere"。
三、环境准备:建表 SQL
下面这段在 SQL Server 2008 上建演示表,直接用查询分析器执行即可(示例对象运行时也会自己建一次,这里先给你看清楚表结构):
- -- SQL Server 2008 语法:identity 自增 + decimal(12,2) 金额 + getdate() 默认时间
- if object_id('dbo.t_pbidea_demo') is not null drop table dbo.t_pbidea_demo
- go
- create table dbo.t_pbidea_demo(
- id int identity(1,1) primary key,
- code varchar(20) not null,
- name nvarchar(50) not null,
- qty int not null default 0,
- price decimal(12,2) not null default 0,
- crt_time datetime not null default getdate()
- )
- go
复制代码
前置:库列表挂 sql.pbl + websuite.pbl,运行目录放 PbIdea.dll / Pbidea_cs.dll。
四、第一个可运行示例:连接 + 建表
前置:新建 NVO nvo_dbpool_demo,库列表含 sql.pbl 与 websuite.pbl;运行目录已放 PbIdea.dll。
步骤:1) 把下面代码贴进 nvo_dbpool_demo;2) 在窗口按钮的 clicked 里写 nvo_dbpool_demo lnvo / lnvo = create nvo_dbpool_demo / lnvo.of_demo() / destroy lnvo;3) 运行点击,MessageBox 会输出每一步的执行结果。 - // ===== 输入区:按你的实际环境修改 =====
- uo_database ldb // uo_database 连接对象
- string ls_server // 服务器地址与端口,端口用逗号分隔
- string ls_db // 数据库名
- string ls_user // 登录名
- string ls_pwd // 登录口令
- ls_server = '127.0.0.1,18433'
- ls_db = 'tempdb'
- ls_user = 'sa'
- ls_pwd = 'Zkkt@667788'
- // 功能:准备一条 uo_database 连接(此时还没连,Open() 才真正建立连接)
- // 要点:uo_database 继承自 transaction,属性名与 SQLCA 完全一致
- ldb = create uo_database
- ldb.DBMS = 'MSS' // SQL Server;等价写法 'Microsoft SQL Server'
- ldb.ServerName = ls_server // 端口用逗号分隔,不能用冒号
- ldb.Database = ls_db
- ldb.LogId = ls_user
- ldb.LogPass = ls_pwd
- ldb.AutoCommit = true // true=自动提交;false=需手工 CommitTrans/RollbackTrans
- // 功能:建表(SQL Server 2008 语法)
- // 要点:DDL 没有结果集,成败只能看 adb.SQLCode / adb.SQLErrText
- uo_recordset lrs
- string ls_ret
- ls_ret = ''
- // PB 字符串里含单引号时,外层改用双引号包,不要写成 '' 双写(那是 T-SQL 不是 PowerScript)
- lrs = ldb.ExecuteSQL("if object_id('dbo.t_pbidea_demo') is not null drop table dbo.t_pbidea_demo")
- if ldb.SQLCode <> 0 then
- ls_ret = 'DROP_FAIL code=' + String(ldb.SQLCode) + ' ' + ldb.SQLErrText
- else
- lrs = ldb.ExecuteSQL('create table dbo.t_pbidea_demo(id int identity(1,1) primary key, code varchar(20) not null, name nvarchar(50) not null, qty int not null default 0, price decimal(12,2) not null default 0, crt_time datetime not null default getdate())')
- if ldb.SQLCode <> 0 then
- ls_ret = 'CREATE_FAIL ' + ldb.SQLErrText
- else
- ls_ret = 'DDL_OK'
- end if
- end if
- MessageBox('建表结果', ls_ret)
复制代码
实测输出:DDL_OK。
五、参数化写入:三种写法
这里是本文最容易踩坑的地方,先说结论:
PBIDEA 的参数占位符是 :1 / :2 或 :名字,不是 ?。
? 是 PB 原生动态 SQL 的写法,搬到 uo_database 上会报「至少一个参数没有被指定值」。
三种写法全部实机跑通:
- // ===== 输入区:写入内容与连接 =====
- string ls_code // 商品编码
- string ls_name // 商品名称
- long ll_qty // 数量
- double ld_price // 单价(注意:用 double,不要用 decimal)
- uo_recordset lrs
- string ls_ret
- boolean lb_ok
- uo_database ldb // 已 Open 的连接
- ls_code = 'A001'
- ls_name = '鼠标'
- ll_qty = 10
- ld_price = 59.50
- ls_ret = ''
- // 方式一:ExecuteSQL 变参,占位符 :1 :2 :3 :4
- // 坑:变参位置不要直接写字面量,要用【已声明类型的变量】传,否则 DLL 认不出参数类型
- lrs = ldb.ExecuteSQL('insert into dbo.t_pbidea_demo(code,name,qty,price) values(:1,:2,:3,:4)', ls_code, ls_name, ll_qty, ld_price)
- if ldb.SQLCode <> 0 then
- ls_ret = 'INS1_FAIL ' + ldb.SQLErrText
- else
- ls_ret = 'INS1 rows=' + String(ldb.RowsAffected())
- end if
- // 方式二:PrepareSQL + 按位置绑定(位置从 1 开始)
- ls_code = 'A002'
- ls_name = '键盘'
- ll_qty = 5
- ld_price = 129.00
- lrs = ldb.PrepareSQL('insert into dbo.t_pbidea_demo(code,name,qty,price) values(:1,:2,:3,:4)')
- lrs.BindParam(1, ls_code)
- lrs.BindParam(2, ls_name)
- lrs.BindParam(3, ll_qty)
- lrs.BindParam(4, ld_price)
- lb_ok = lrs.ExecuteSQL()
- if lb_ok then
- ls_ret = ls_ret + ' INS2 rows=' + String(lrs.RowsAffected())
- else
- ls_ret = ls_ret + ' INS2_FAIL ' + ldb.SQLErrText
- end if
- // 方式三:PrepareSQL + 按名字绑定
- ls_code = 'A003'
- ls_name = '显示器'
- ll_qty = 2
- ld_price = 899.00
- lrs = ldb.PrepareSQL('insert into dbo.t_pbidea_demo(code,name,qty,price) values(:code,:name,:qty,:price)')
- lrs.BindParam('code', ls_code)
- lrs.BindParam('name', ls_name)
- lrs.BindParam('qty', ll_qty)
- lrs.BindParam('price', ld_price)
- lb_ok = lrs.ExecuteSQL()
- if lb_ok then
- ls_ret = ls_ret + ' INS3 rows=' + String(lrs.RowsAffected())
- else
- ls_ret = ls_ret + ' INS3_FAIL ' + ldb.SQLErrText
- end if
- MessageBox('写入结果', ls_ret)
复制代码
实测输出:INS1 rows=1 INS2 rows=1 INS3 rows=1。
三种怎么选:
| 写法 | 适用场景 | 备注 | | ExecuteSQL 变参 | 一次性语句,图省事 | 散参或 any 数组都行,数组形式适合 in(:args) | | PrepareSQL + 位置绑定 | 同一语句循环执行多次 | 编译一次、绑定多轮,性能最好 | | PrepareSQL + 名字绑定 | SQL 里参数多、顺序易错 | 可读性最好 |
BindParam 还有几个实用重载:出参必须带 ref(BindParam(1, ref ls_out, 0),nSize 为 0 时默认 1024);BindParamFile(pos, fileName) 可以直接把文件内容/URL 绑进字段;SetBlobBindType(0..4) 控制 blob 的绑定方式。
六、取数:uo_recordset 全貌
ExecuteSQL 返回的是 uo_recordset,destructor 会自动 Close()。
- uo_recordset lrs
- uo_json ljsons[]
- string ls_ret
- string ls_code
- string ls_name
- double ld_qty
- double ld_price
- long ll_rows
- int li_n
- any la_cnt
- uo_database ldb // 已 Open 的连接
- ls_ret = ''
- ll_rows = 0
- lrs = ldb.ExecuteSQL('select code,name,qty,price,crt_time from dbo.t_pbidea_demo order by id')
- if lrs.HasResultSet() then
- do while lrs.FetchNext()
- ls_code = lrs.GetString('code') // 按字段名取
- ls_name = lrs.GetString(2) // 按序号取(从 1 开始)
- ld_qty = lrs.GetNumber('qty')
- ld_price = lrs.GetNumber('price')
- ll_rows = ll_rows + 1
- ls_ret = ls_ret + '~r~n ROW ' + ls_code + ' ' + ls_name + ' qty=' + String(ld_qty) + ' price=' + String(ld_price)
- loop
- ls_ret = 'QUERY rows=' + String(ll_rows) + ls_ret
- else
- ls_ret = 'QUERY NO_RESULTSET ' + ldb.SQLErrText
- end if
- // 转 JSON:整个结果集一次性取成 uo_json 数组
- // 注意:上面的 do while 已经把 lrs 读完了,必须重新 ExecuteSQL 拿一个新结果集
- lrs = ldb.ExecuteSQL('select top 2 code,name,qty from dbo.t_pbidea_demo order by id')
- li_n = lrs.FetchJsons(ljsons)
- if li_n > 0 then
- ls_ret = ls_ret + '~r~n JSONS n=' + String(li_n) + ' first=' + ljsons[1].ToString()
- else
- ls_ret = ls_ret + '~r~n JSONS n=0'
- end if
- // 标量查询:只要一个值就用 ExecuteScaler
- la_cnt = ldb.ExecuteScaler('select count(*) from dbo.t_pbidea_demo')
- ls_ret = ls_ret + '~r~n SCALAR count=' + String(la_cnt)
- MessageBox('取数结果', ls_ret)
复制代码
实测输出:
- QUERY rows=3
- ROW A001 鼠标 qty=10 price=59.5
- ROW A002 键盘 qty=5 price=129
- ROW A003 显示器 qty=2 price=899
- JSONS n=1 first=[{"code":"A001","name":"鼠标","qty":10},{"code":"A002","name":"键盘","qty":5}]
- SCALAR count=3
复制代码
注意 FetchJsons 返回的 n 是 1 —— 它返回的是一个整体 uo_json(数组),不是每行一个元素;ljsons[1] 就是整份数据。
取值方法两套重载(按字段名 / 按序号,从 1 开始):GetNumber / GetString / GetBlob / GetDecimal / GetDate / GetTime / GetDateTime,另有 GetValue() 返回 any 由你自判类型。字段元信息走 GetFieldCount() + GetField(idx 或 name) 拿 uo_field(GetName/GetType/GetSize/GetPrecision/GetScale)。
⚠️ uo_field 里取小数的方法源码拼写是 GetDeciaml()(不是 GetDecimal),照抄,别"顺手改正",否则编译不过。
七、事务与隔离级别
AutoCommit = false 之后,所有写操作都进事务,必须显式提交或回滚:
- // ===== 输入区:要写入的一行数据 =====
- string ls_code // 编码
- string ls_name // 名称
- long ll_qty // 数量
- double ld_price // 单价
- uo_recordset lrs
- string ls_ret
- uo_database ldb // 已 Open 的连接
- ls_code = 'B001'
- ls_name = '事务行'
- ll_qty = 1
- ld_price = 9.90
- ls_ret = ''
- ldb.AutoCommit = false
- // 0=ReadUncommitted 1=ReadCommitted 2=RepeatableRead 3=Serializable 4=Snapshot
- ldb.SetTransLevel(1)
- lrs = ldb.ExecuteSQL('insert into dbo.t_pbidea_demo(code,name,qty,price) values(:1,:2,:3,:4)', ls_code, ls_name, ll_qty, ld_price)
- if ldb.SQLCode = 0 then
- ls_ret = 'TX_COMMIT rows=' + String(lrs.RowsAffected())
- ldb.CommitTrans()
- else
- ldb.RollbackTrans()
- ls_ret = 'TX_ROLLBACK ' + ldb.SQLErrText
- end if
- ldb.AutoCommit = true
- MessageBox('事务结果', ls_ret)
复制代码
实测输出:TX_COMMIT rows=1。
两个细节:
- 行数要在 CommitTrans() 之前读。提交之后 adb.RowsAffected() 会变成 -1,想拿行数就读 lrs.RowsAffected()。
- SetTransLevel 的参数是 int,直接写数字即可,不用常量。
八、连接池:uo_database_pool
四个方法,一张表讲清:
| 方法 | 说明 | | CreatePool(string poolName, uo_database db) | 按模板连接建池,池名可多组共用 | | QueryPool(string poolName) | 取一条连接,用完必须还 | | GiveBackPool(uo_database db) | 归还(destroy 也会自动还,二选一) | | DestroyPool() | 销毁池,释放池中全部连接 |
- // 功能:连接池 —— 建池 / 取连接 / 使用 / 归还 / 销毁
- // 要点:QueryPool 取出的连接用完必须 GiveBackPool;
- // 池中连接 10 分钟无活动会被自动断开,长时间闲置后要先 IsAlive() 探活
- uo_database ldb, ldb2
- uo_database_pool lpool
- uo_recordset lrs
- string ls_ret
- int li_ret
- any la_cnt
- ls_ret = ''
- ldb = create uo_database
- ldb.DBMS = 'MSS'
- ldb.ServerName = '127.0.0.1,18433'
- ldb.Database = 'tempdb'
- ldb.LogId = 'sa'
- ldb.LogPass = 'Zkkt@667788'
- ldb.AutoCommit = true
- // 关键坑:CreatePool 的模板连接必须是【已经 Open 成功】的连接。
- // 只 create 不 Open,DLL 会打印 "template db: 00000000",池根本没建起来,
- // 后面 QueryPool 一律返回 "not find name [池名] in pool"
- if not ldb.Open() then
- MessageBox('连接池', 'POOL_TPL_CONNECT_FAIL ' + ldb.SQLErrText)
- destroy ldb
- return
- end if
- lpool = create uo_database_pool
- li_ret = lpool.CreatePool('mss_demo', ldb)
- ls_ret = 'CREATE_POOL ret=' + String(li_ret)
- ldb2 = lpool.QueryPool('mss_demo') // 从池中申请一条连接
- if IsValid(ldb2) then
- ls_ret = ls_ret + ' QUERY_POOL isopen=' + String(ldb2.IsOpen()) + ' alive=' + String(ldb2.IsAlive())
- lrs = ldb2.ExecuteSQL('select count(*) as c from dbo.t_pbidea_demo')
- if lrs.HasResultSet() then
- if lrs.FetchNext() then
- la_cnt = lrs.GetValue('c')
- ls_ret = ls_ret + ' cnt=' + String(la_cnt)
- end if
- end if
- lpool.GiveBackPool(ldb2) // 用完一定要还
- ls_ret = ls_ret + ' GIVEN_BACK'
- else
- ls_ret = ls_ret + ' QUERY_POOL_FAIL'
- end if
- lpool.DestroyPool() // 销毁池,释放池中全部连接
- ls_ret = ls_ret + ' DESTROYED'
- destroy lpool
- ldb.Close()
- destroy ldb
- MessageBox('连接池结果', ls_ret)
复制代码
实测输出:CREATE_POOL ret=1 QUERY_POOL isopen=true alive=true cnt=4 GIVEN_BACK DESTROYED。
池的注册是 DLL 级别按名字的:同一个名字在任何地方 QueryPool 都能取到(PBIDEA 自带的 SQL 工作台就是 CreatePool 之后立刻 destroy pool,后面另建 uo_database_pool 照样能取)。这也意味着程序启动时最好先 DestroyPool() 清一次,避免上一轮残留。
九、连接配置复用
不想每次都写一堆赋值,就先连一次再存盘:
- // 连成功后把属性存成配置文件,以后 Open(文件) 直连
- uo_database ldb
- ldb = create uo_database
- ldb.DBMS = 'MSS'
- ldb.ServerName = '127.0.0.1,18433'
- ldb.Database = 'tempdb'
- ldb.LogId = 'sa'
- ldb.LogPass = 'Zkkt@667788'
- if ldb.Open() then
- ldb.SaveConfig('db.cfg') // 存盘
- ldb.Close()
- end if
- destroy ldb
- // 以后:
- uo_database ldb2
- ldb2 = create uo_database
- if ldb2.Open('db.cfg') then
- MessageBox('配置连接', 'OK db=' + ldb2.Database)
- end if
- destroy ldb2
复制代码
十、坑清单(全部实机踩过)
| # | 坑 | 症状 | 解法 | | 1 | 占位符写成 ? | 「至少一个参数没有被指定值」+ invalid index 1..n | 改成 :1 :2 或 :名字 | | 2 | 变参直接写字面量 | unknwn param!!!,字段被插成 NULL | 先声明类型化变量再传 | | 3 | 用 decimal 绑定 | unknwn param!!!,列不允许 Null | 改用 double | | 4 | CreatePool 传未 Open 的连接 | template db: 00000000、not find name in pool | 模板连接必须先 Open() 成功 | | 5 | 结果集读完再 FetchJsons | 返回 n=0 | 重新 ExecuteSQL 拿新结果集 | | 6 | CommitTrans() 后读行数 | RowsAffected() = -1 | 提交前读,或读 lrs.RowsAffected() | | 7 | PB 字符串里双写单引号 '' | C0031 语法错误 | 外层改双引号,或用 ~' | | 8 | 库列表只挂 sql.pbl | C0001: Illegal data type: uo_json | websuite.pbl 一起挂 | | 9 | 池连接闲置 10 分钟 | 自动断开,再用报错 | 用前 IsAlive() 探活 | | 10 | uo_field.GetDeciaml() 拼写 | 写成 GetDecimal 编译不过 | 照抄源码拼写 GetDeciaml | | 11 | MSSQL 绑 BLOB | 绑定失败 | 加 DBParm:UseStreamForLongOrLobParameters="0";ICommandPrepare="SetParameterInfo" | | 12 | Oracle 连接 | 报线程安全错 | 设 ThreadSafe="Yes" |
十一、对比:三条连库路线怎么选
| 维度 | SQLCA(PB 原生) | uo_database(PBIDEA) | pblit LIT * | | 归属 | PB 自带 | PBIDEA sql.pbl,需 PbIdea.dll | pbnpm 的 pblit 驱动 | | 多实例 | 需自己声明 transaction | create uo_database 随手开 | 同左 | | 参数化 | 原生动态 SQL 四步式 | :1 变参 / BindParam | 依赖驱动 | | 连接池 | 无 | uo_database_pool 原生支持 | 无 | | 依赖 | 无 | PbIdea.dll + sql.pbl + websuite.pbl | pblit{版本}.dll | | 适用 | 最简 Demo / 老工程 | 新项目推荐 | 想绕开外部依赖时 |
顺带一提,uo_database 的 DBMS 也支持 "SQLite",如果你只是想本地存点数据、不想牵扯 SQL Server,把 DBMS 换成 SQLite、Database 换成 db 文件路径即可,其余代码一行不改。
十二、扩展点
- 多库切换:按名字建多个池(CreatePool('ora', ldbOra)、CreatePool('mss', ldbMss)),业务代码用池名取连接,切换数据源只改建池那一处。
- 配合 uo_json:FetchJsons 直接产出 uo_json,可以无缝接给 uo_httpclient 发走,或 WriteJsonFile 落盘,做接口/导出很省事。
- 配合 uo_database 的事件:beginconnect / endconnect / ondisconnect 里挂日志,就能不侵入业务地做连接监控。
- 分页:SQL Server 2008 用 ROW_NUMBER() 包一层即可,例如
select * from (select ROW_NUMBER() over(order by id) as rn, * from dbo.t_pbidea_demo) t where rn between 1 and 10。
实机验证情况(本文代码已全部跑通)
| 项 | 结果 | | 编译环境 | PowerBuilder 12.5,pypower pbl import 导入 nvo_dbpool_demo.sru(GBK/CRLF/无 BOM) | | 全量编译 | build rebuild --type full → 0 错误 | | 运行验证 | PBVM 真机 pypower test → PASS,输出 DDL_OK / INS1 rows=1 INS2 rows=1 INS3 rows=1 / QUERY rows=3 / JSONS n=1 / SCALAR count=3 / TX_COMMIT rows=1 / CREATE_POOL ret=1 ... cnt=4 / DROP_END_OK | | 数据库 | SQL Server 2008 R2 SP3(10.50.6000.34),127.0.0.1,18433 | | 编码检查 | check_enc.py bad=0;check_pb125.py 0 错误 0 提醒 | | PB10 与 PB12.5 | 四个对象(uo_database / uo_recordset / uo_field / uo_database_pool)两版源码逐字节相同,API 无差异,示例以 PB10 为基准书写,PB12.5 直接复用 |
第九节 SaveConfig 那段为静态核对(配置文件路径按你的部署目录调整),其余全部为实机跑通。 |