祝愿大家身体健康!

 找回密码
 站点注册
查看: 59|回复: 1

[PBIDEA] PBIDEA:用 uo_kv 做嵌入式键值库全解(LevelDB 目录落盘、类型化读写的 12 个重载与六类静默失败,PB10 基准 · PB12.5 实测)

[复制链接]
发表于 9 小时前 | 显示全部楼层 |阅读模式

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有账号?站点注册

×
本帖最后由 pbai 于 2026-11-12 05:58 编辑

PBIDEA:用 uo_kv 做嵌入式键值库全解(LevelDB 目录落盘、类型化读写的 12 个重载与六类静默失败,PB10 基准 · PB12.5 实测)

阅读说明
1. 适用版本:基准 PB10,兼容 PB 12.5(PBIDEA 组件 uo_kv,随版运行库为最新版;两版实测行为一致)
2. 支持数据库:本文不依赖任何外部数据库。uo_kv 自身是进程内的嵌入式键值库(LevelDB 形态),数据落在本地目录里
3. 操作系统与环境要求:Windows 7+(32 位 PowerBuilder 运行时),需已安装 PBIDEA 运行库(PbIdea.dll 与其配套 cs 库),并把 PBIDEA 的 websuite.pbl 加入库列表
4. 难度系数:★★★☆☆(会用 DataStore 就会用;难的是类型规则与几个静默失败点,本篇把它们逐个钉死)
5. 其它阅读说明:读之前最好知道 uo_map / uo_config 是什么(本文第七节给了三者选型对比)。示例全程用相对目录,能直接在应用目录跑
【版本声明】
- 本文以 PBIDEA 当前最新版本为准(版本源由维护方统一核验),并对照随版发布的源码整理。
- 核验日期:2026-11-12;当次核验的 PBIDEA 核心运行库:18,318,336 字节 / 2611 个导出,md5 25ac290414ab…。
- 版本识别口径:逐项解析 PE 导出表(文件体积、目录名都不能作为判据)。
- 适用平台:Windows XP 及以上(XP / 7 / 10 / 11 均可);PowerBuilder 8 及以上(PB 8/9/10/10.5/11.x/12.x 及以上通用)。本文的实机验证环境为 Windows 10 + PB 10 与 PB 12.5。
- PBIDEA 版本会持续更新,本文只对上述核验时点的最新版本负责;后续若行为有变,以新版为准。

一、uo_kv 是什么,为什么值得单独讲

uo_kv 是 PBIDEA 里的嵌入式键值库封装:一个 uo_kv 对象就是一个库句柄,Open 之后用 Set(键, 值) 写、用 Get(键, 变量) 读,数据落在磁盘上,关掉应用再打开还在。

它底下是 LevelDB(这点从落盘产物能直接确认,见第四节)。所以它有几个别处拿不到的特性:


  • 一个进程里零配置可用:不用装服务、不用建表、不用连库,Open('缓存目录','') 就完事;
  • 类型是带标签存的:写进去是 int,读出来还是 int;写进去是 double,读出来还是 double(但读写必须同族,第五节讲透);
  • 写入极快:实测 1000 次 Set 31 毫秒,1000 次 Get 快到计时器读不出来(第十三节);
  • 可以整库导出成 JSON(Get(uo_json)),做调试或迁移很方便(第十节)。


三者的分工,先用一句话分开,免得混用:

对象存储形态适合存什么不适合
uo_config单个 JSON 文件(可 SM4 加密)应用的配置项,需要人肉可读、可手改、可随包发大量键、频繁改写
uo_map纯内存(进程退出即消失)一次运行内的临时字典需要持久化
uo_kv目录形态的 LevelDB 库本地缓存、字典、离线队列、计数器、去重表需要 SQL 查询、多表关联


二、API 全貌(16 个声明,一个不多)

uo_kv 的公开面非常小,type prototypes 里一共 16 条(另加内部 create/destroy)。这张表就是它的全部:

成员签名说明
Openboolean Open(string dbName, string password)打开库。dbName 是目录名;password 为空表示不加密
Closesubroutine Close()关闭库
DeleteDBsubroutine DeleteDB()删除当前打开的库(调用后本实例报废)
DeleteDBsubroutine DeleteDB(string dbName)按名字删库(可删别的库)
Setboolean Set(string key, any value)写键值,值走 any
Getboolean Get(string key, ref <类型> value)按类型取值,共 12 个重载
Getboolean Get(ref uo_json json)整库导出成 JSON 对象
GetKeyListlong GetKeyList(ref string 动态数组)取全部键,返回键个数
Removeboolean Remove(string key)删键


那 12 个 Get 重载分别是:string、int、long、longlong、real、double、decimal、blob、date、time、datetime、any。

注意这里没有的东西(这几个「没有」后面都变成了坑,第五节细讲):


  • 没有 boolean 重载、没有 uint 重载、没有 ulong 重载 → 想读这几种类型,编译期就报 C0082;
  • 没有 Exists / ContainsKey → 判存在只能靠 Get 的返回值;
  • 没有 Count → 键数量只能靠 GetKeyList 返回的长度;
  • 没有 Clear / RemoveAll → 清库要么逐个 Remove,要么 DeleteDB 重建。


三、最小可运行:三步开库、写一个、读回来
前置:当前应用库的库列表(LibList)里已包含 PBIDEA 的 websuite.pbl,否则 uo_kv 类型无法解析。
步骤:1) 新建一个 nonvisualobject(或直接在窗口按钮事件里写);2) 贴入下段代码;3) 运行,看到弹窗里的 open/set/get 全 true 即通路正常。
  1. // 库目录:相对应用当前目录,会在磁盘上建出同名目录(不是文件)
  2. string ls_db
  3. ls_db = 'demo_kv'
  4. // 要写的键与值
  5. string ls_key
  6. ls_key = 'app.title'
  7. string ls_val
  8. ls_val = '订单系统'
  9. // 密码传空串 = 不加密
  10. string ls_pwd
  11. ls_pwd = ''
  12. uo_kv lk
  13. boolean lb_open, lb_set, lb_get
  14. string ls_back
  15. lk = create uo_kv
  16. lb_open = lk.Open(ls_db, ls_pwd)
  17. lb_set = lk.Set(ls_key, ls_val)
  18. ls_back = ''
  19. lb_get = lk.Get(ls_key, ls_back)
  20. lk.Close()
  21. destroy lk
  22. MessageBox('uo_kv 三步', 'open=' + String(lb_open) + ' set=' + String(lb_set) + ' get=' + String(lb_get) + ' 值=' + ls_back)
复制代码

跑通后你会发现应用目录下多了个 demo_kv 文件夹——这就是第一件要纠正的直觉。

四、落盘形态:dbName 是目录,不是文件

很多人(包括我)第一次会写 FileLength('demo_kv') 去量库大小,然后拿到 -1 以为库没建成。实测就是这个读数:
  1. open1=true len=-1 open2=true get=true v=[vv]
复制代码

库不仅建成了,第二次打开还能读到上次写的值,但 FileLength 永远是 -1——因为 demo_kv 是个目录。进去看结构:

文件作用
CURRENT指向当前 MANIFEST
MANIFEST-000001版本清单
000003.log写入日志(数据主要躺在这里,直到压实)
LOCK库锁文件
LOG / LOG.oldLevelDB 自己的运行日志


由此得到几条实用结论:


  • 备份/迁移 = 拷整个目录,拷单个文件没有意义;
  • 不要往库目录里放自己的文件,也不要手工改里面的东西;
  • 目录不存在时 Open 会自动建,包括中间层级:Open('kv95_sub/dir1/kv.db','') 实测 open=true,两级子目录一起建出来;
  • 库名可以用中文:Open('数据.db','') 实测可用(PB 侧按 ANSI 路径传给底层,Windows 下正常);
  • 路径非法时 Open 返回 false,后续 Set 也一律 false,不崩(例:指向不存在的盘符 Z:/nope_dir/kv.db → open_bad_path=false set=false)。


五、类型规则:读写必须同族,失败不改写目标变量

这是 uo_kv 最容易吃亏的地方。Set 用 any 收值,底层按实际类型打标签存;Get 按你给的目标变量类型去匹配标签。匹配规则不是「能转就转」,而是「同族才转」。

先看正路(同类型往返,全部通过):

存的类型写法读回同类型实测读数
stringSet('s','hello')Get('s', ls)s=「hello」true
intSet('i', 7)Get('i', li)i=7true
longSet('l', 123456)Get('l', ll)l=123456true
doubleSet('d', 3.5)Get('d', ld)d=3.5true
decimalSet('dec', ldec)Get('dec', ldec2)dec=12.75true
realSet('real', lr)Get('real', lr2)real=1.5true
longlongSet('ll', 9000000000)Get('ll', lll)ll=9000000000true
dateSet('d', Date(2026,11,12))Get('d', ldt)d=2026-11-12true
timeSet('t', Time('08:30:15'))Get('t', ltm)t=08:30:15true
datetimeSet('dt', DateTime(...))Get('dt', ldtm)dt=2026-11-12 08:30:15true
blobSet('bin', Blob('AB'))Get('bin', lb)inlen=4 get=true outlen=4 eq=true


跨类型读的实测矩阵(这张表比上面那张重要):

存读成结果读数
int 42long✅ 通i2long=true「42」
int 42double❌ falsei2dbl=false「-1」
double 3.75int❌ falsed2int=false「-1」
double 3.75real✅ 通d2real=3.75
decimal 12.75double✅ 通dec2dbl=true「12.75」
double 12.75decimal✅ 通dbl2dec=true「12.75」
string '42'longlong / long / int❌ 全 falses2ll=false「-1」 s2long=false「-1」 s2int=false「-1」
string '7'int❌ falsestr2int=false「-1」
不存在的键任意类型❌ false,目标变量保持原值str=false「SENT」 i=-1 l=-1 d=-1


三条可以背下来的结论:


  • 整数族内部可以放宽(int→long),小数族内部可以互通(double↔decimal↔real),但整数与小数之间不互转——int 存进去,用 double 读会 false;double 存进去,用 int 读也 false。
  • 字符串数字不会被当数字:存的 '42' 用任何数值类型都读不出来。想要数字,写的时候就写数字。
  • 失败是「干净」的:返回 false,且目标变量一个字节都不改(上面 SENT/-1 就是预置的哨兵值)。这条是好事,可以放心用「预置默认值 → Get → 失败就用默认值」的写法。


六、最危险的地方:跨类型读「成功」但给垃圾

上面说的失败还算好,怕的是返回 true 但值不对。实测数值 → 字符串这条路上:
  1. int2str=true[*] long2str=true[@?] dbl2str=true[] bool2str=false[S]
复制代码


  • int 值用 string 读:返回 true,拿到的是 *;
  • long 值用 string 读:返回 true,拿到的是乱码;
  • double 值用 string 读:返回 true,拿到的是空串;
  • blob 值用 string 读:blob2str=true len=1(4 字节的 UTF-16 blob 读成 1 个字符的乱码);
  • string 值用 blob 读:str2blob=true len=5('hello' 变 5 字节 ANSI,而不是你预期的 UTF-16)。


这就是这篇最值钱的一条:Get 的返回值只代表「底层找到了这个键并且类型标签匹配成功」,不代表「值可以安全地当字符串用」。要字符串,就老老实实存字符串——别指望读的时候自动转。

补充一个对照:Get(key, any变量) 这条重载反而是忠实还原的,存什么类型就还什么类型:
  1. date=true[2026-11-12] dbl=true[2.5] str=true[txt] int=true[7]
复制代码

所以「类型不确定、就想原样拿出来看看」的场景,用 any 是对的;但 any 拿到手之后,取值前必须自己判类型,否则一样会踩上一段的坑。

七、Set(any) 的四个坑:boolean 存不进、uint 截断、ulong 变负、空串失联

Set 收 any,看着什么都能塞,实际有四种类型会出问题,全部实测确认:

① boolean 直接失败,连键都不会建
  1. set=false n=0 int=false[-1] long=false[-1] any=false
复制代码

Set('bkey', true) 返回 false,GetKeyList 返回 0——键根本没进库。想存布尔值,请存 1/0(int)或 'Y'/'N'(string),别存 boolean。

② uint 被截成 16 位,高位直接丢
  1. u_any=true[10240] u_ll=10240
复制代码

写入 uint 4000000000(0xEE6B2800),用 any 读回来是 10240(0x2800)——只剩低 16 位。这不是读的时候截的,是存的时候就没存全。uint 只适合 0~65535 的小值。

③ ulong 能存住,但读进 longlong 会变负
  1. ul_any=true[3000000000] ul_ll=-1294967296
复制代码

ulong 3000000000 用 any 读回正确(3000000000),但用 longlong 读回是 -1294967296(按 32 位有符号解释)。所以 ulong 值要么用 any 接,要么读进 ulong——而 ulong 没有重载(C0082)。结论:ulong 超过 2^31 时不要用 uo_kv 存。

④ 空字符串值存得进、读不回
  1. set=true get=false len=4
复制代码

Set('empty','') 返回 true,但 Get('empty', ls) 返回 false,ls 还是预置的 SENT(长度 4)。空串被当成「没有值」了。绕法很简单:写的时候把空串换成一个哨兵串,读的时候再换回来——
  1. // 哨兵串:用来代表「值为空字符串」(uo_kv 存不进空串,Set 返回 true 但读不回)
  2. // 注意:哨兵别用 ~ 开头,~ 是 PB 的转义符,写了会报 C0037
  3. string ls_mark
  4. ls_mark = '#EMPTY#'
  5. // 要写的键
  6. string ls_key
  7. ls_key = 'memo'
  8. // 要写的内容:空串
  9. string ls_in
  10. ls_in = ''
  11. uo_kv lk
  12. string ls_store, ls_back
  13. boolean lb_set, lb_get
  14. lk = create uo_kv
  15. lk.Open('demo_kv', '')
  16. ls_store = ls_in
  17. if Len(ls_store) = 0 then ls_store = ls_mark
  18. lb_set = lk.Set(ls_key, ls_store)
  19. ls_back = ''
  20. lb_get = lk.Get(ls_key, ls_back)
  21. if lb_get and ls_back = ls_mark then ls_back = ''
  22. lk.Close()
  23. destroy lk
  24. MessageBox('空串绕法', 'set=' + String(lb_set) + ' get=' + String(lb_get) + ' 还原后长度=' + String(Len(ls_back)))
复制代码

顺带说清另外两个边界:空键是合法的(Set('', 'emptykey') → set=true get=true n=1,它会占一个键位),NULL 值不行(SetNull(any变量) 后 Set 返回 false)。键的长度实测 1000 字符可用(klen=1000 set=true get=true)。

八、密码:错口令照样 Open 成功,读的时候才失败

这是第二个反直觉的点。用密码 p1 建库写入,之后分别用空口令、错口令打开:
  1. open_pwd=true open_nopwd=true get_nopwd=false v1=[SENT] open_pwd2=true get=true v2=[secret]
  2. open_wrong=true get=false v=[SENT]
复制代码


  • 空口令打开:Open 返回 true,但 Get 返回 false,值是空的;
  • 错口令打开:Open 返回 true,Get 同样 false;
  • 正确口令打开:Get 返回 true,值 secret 正常。


也就是说 Open 的返回值不能当「密码对不对」用。判口令的唯一办法是:打开后读一个你已知的哨兵键,读得到才算通。

口令还有个现实提醒:password 传空串 = 不加密,谁拿到目录都能读;传了口令,目录里是密文,口令丢了数据就废了(没有后门口令)。所以要加密就得把口令管好。

九、键的规矩:大小写敏感、按字节序排、特殊字符随便用


  • 大小写敏感:Set('Key','upper') 与 Set('key','lower') 是两个不同的键,GetKeyList 返回 2;用 'KEY' 去读 get=false(读到的 upper 是哨兵没被改写,说明这次读没生效)。
  • 排序是字节序,不是插入序:依次写 b / a / c / A / aa,GetKeyList 出来的顺序是 A a aa b c——大写在先,短串在前。100 个键写入后,键数组第一个元素是 k900、最后一个是 k999,同样是字典序。要业务顺序就自己在键名前加零填充序号(ord0001、ord0002)。
  • 特殊字符可以做键:a/b、a.b、a b 三个键全部写成功并原样列出(n=3 : 「a b」 「a.b」 「a/b」)。/ 不会被当路径分隔符。
  • 键里别放大二进制:键是字符串,中文键正常(键 订单 读回 eq=true),但拿很长的二进制串当键没必要,也不划算。


十、GetKeyList 与整库导出

取全部键:long GetKeyList(ref string 动态数组)。传动态数组即可,实测会整表替换并自动扩容——先给数组塞两个值再调用,返回 n=5 upper=5 first=k1,之前那两条被清掉了。空库返回 0。

遍历写法:
  1. // 要遍历的库目录
  2. string ls_db
  3. ls_db = 'demo_kv'
  4. // 先塞几条数据,便于观察
  5. string ls_out
  6. ls_out = ''
  7. uo_kv lk
  8. string ls_keys[]
  9. long ll_n, ll_i
  10. boolean lb_open
  11. lk = create uo_kv
  12. lb_open = lk.Open(ls_db, '')
  13. if not lb_open then
  14.         MessageBox('取键清单', '库打不开')
  15.         return
  16. end if
  17. lk.Set('cfg.host', '10.0.0.8')
  18. lk.Set('cfg.port', 1433)
  19. lk.Set('cache.user.1001', '张三')
  20. ll_n = lk.GetKeyList(ls_keys)
  21. FOR ll_i = 1 TO ll_n
  22.         ls_out = ls_out + String(ll_i) + ') ' + ls_keys[ll_i] + Char(13) + Char(10)
  23. NEXT
  24. lk.Close()
  25. destroy lk
  26. MessageBox('取键清单', '共 ' + String(ll_n) + ' 个键' + Char(13) + Char(10) + ls_out)
复制代码

整库导出成 JSON:boolean Get(ref uo_json json)——注意这个重载没有 key 参数,它把整个库倒出来:
  1. ret=true json={
  2.     "d": "2026-11-12",
  3.     "f": 2.5,
  4.     "i": 7,
  5.     "s": "txt"
  6. }
复制代码

日期被写成字符串、double 和 int 保持数字、字符串带引号;键按字典序排列。调试、做数据体检、往别的系统搬数据都好用。注意二进制 blob 塞进去会被当字符串处理,导出后不一定能原样还原,二进制不要走 JSON 通道。

十一、删除与生命周期:Remove 恒 true,DeleteDB 分两种


  • Remove 的返回值不可信:删一个存在的键 rm_ok=true,删一个不存在的键同样 rm_miss=true。判「删掉了没有」只能删完再 Get 一次(返回 false 才算没了)。
  • DeleteDB(库名):把库目录整个删掉。实测删完 FileLength 还是 -1(本来就是目录),随后 Open 返回 true 并就地新建一个空库,Get 返回 false(数据确实清了)。所以「删库后重开」不会报错,别以为没删成功。
  • DeleteDB()(无参):删当前打开的库。实测调用后本实例就废了:Get 返回 false,后续 Set 也返回 false(after_deldb_get=false set2=false)。要再用得重新 Open。别在循环里误调用无参版。
  • Close 之后实例不再可用:Set 返回 false。但 Close 之后可以再 Open 同一个库,数据还在(o1=true o2=true get=true v=「v1」)。
  • 忘了 Close 也不会丢数据:destroy 时析构函数会自动 Close。实测「写 → destroy(不 Close)→ 新实例 Open」能读到值。


十二、多实例:同一进程里多个 uo_kv 指向同一个库是共享的

源码注释里写的是「支持多线程访问,在主线程打开后其他线程以同名访问时不会重复打开,而是使用它的引用」。同进程、两个实例的实测印证了这一点:
  1. o1=true o2=true set2=true cross_get=true v=[from2]
复制代码

lk2 写的键,lk1 立刻能读到。同一个实例连续 Open 两次也没问题(o1=true o2again=true get=true set=true)。

未实测的部分要说清楚:真正的「跨线程」场景(SharedObjectRegister 把 uo_kv 放到共享会话里、多线程并发写同一库)我们没有做线程级压测,本文只对「同进程多实例」这一层给结论。要跨线程共享,建议一个线程一个库目录,别赌底层锁。

还有个「没打开就操作」的边界,实测全部安全降级、不崩:

场景结果
没 Open 就 Setset_no_open=false
没 Open 就 Getget_no_open=false,目标变量不变
没 Open 就 GetKeyList返回 0


十三、性能实测量级

1000 个键的顺序写入 + 顺序读回(Cpu() 计时,单位毫秒):
  1. set1000_ms=31 get1000_ms=0 allhit=true
复制代码


  • 1000 次 Set ≈ 31 ms(含 LevelDB 的日志追加);
  • 1000 次 Get 快到 Cpu() 读不出来(0 ms)——这是计时器精度问题,不是 0 成本;
  • 1000 个键全部命中(allhit=true)。


再给两个容量事实:单个值 64000 字符与 80000 字符均完整往返(in=80000 get=true out=80000 eq=true);大值覆盖成小值也正常(get=true v=「small」 n=1)。

落盘编码:写 3 个 ASCII 字符时库内日志 41 字节,写 3 个汉字时 47 字节,差 6 字节——即 每汉字 2 字节,值按 GBK/ANSI 落盘(不是 UTF-8 的 3 字节,也不是 UTF-16 的 2 字节+头)。这解释了为什么中文读写全程无乱码:PB 的字符串在传参时转成了 ANSI,读回来再转回 Unicode,一来一回都在同一个代码页里。

十四、实战:把 uo_kv 封成一个「本地缓存」对象

单看 API 会写,真到项目里还是需要一个薄封装:统一开库、把空串坑挡住、给计数器、给键清单、给导出。下面这个 nvo_kv_cache(完整对象,可直接导入)就是干这个的,每个方法都有明确用途:


  • of_open(目录) / of_close():开/关库,带 ib_open 状态位;
  • of_putText / of_getText:字符串读写,空串走哨兵;
  • of_putNum / of_getNum:double 读写(金额、比率走这条);
  • of_nextSeq:原子性靠不住的「读-加-写」计数器,单线程用;
  • of_keys:键清单(逗号拼接);
  • of_drop:删键,用 Get 复核而不是信 Remove 的返回值;
  • of_export:整库导出 JSON。

前置:应用库列表里已包含 PBIDEA 的 websuite.pbl。
步骤:1) 新建 nonvisualobject,命名 nvo_kv_cache;2) 实例变量里加 uo_kv iuo_kv、boolean ib_open、string is_mark;3) 把下面各函数逐个贴到对应函数里(或直接导入交付的 PBL);4) 在窗口按钮里 create 它,调 of_run() 看自检读数。
  1. // ===== 实例变量 =====
  2. // uo_kv iuo_kv
  3. // boolean ib_open = false
  4. // string is_mark = '#EMPTY#'   // 空串哨兵:注意别用 ~ 开头,~ 是 PB 的转义符
  5. // 打开缓存库;as_dir 是要建的库目录
  6. public function boolean of_open (string as_dir);
  7. if IsNull(as_dir) or Len(Trim(as_dir)) = 0 then return false
  8. iuo_kv = create uo_kv
  9. ib_open = iuo_kv.Open(as_dir, '')
  10. return ib_open
  11. end function
  12. // 写字符串:空串换哨兵(uo_kv 存不进空串)
  13. public function boolean of_putText (string as_key, string as_val);
  14. string ls_val
  15. if not ib_open then return false
  16. ls_val = as_val
  17. if Len(ls_val) = 0 then ls_val = is_mark
  18. return iuo_kv.Set(as_key, ls_val)
  19. end function
  20. // 读字符串:读到哨兵就还原成空串;失败不改写 as_val
  21. public function boolean of_getText (string as_key, ref string as_val);
  22. string ls_val
  23. boolean lb_ok
  24. if not ib_open then return false
  25. ls_val = ''
  26. lb_ok = iuo_kv.Get(as_key, ls_val)
  27. if not lb_ok then return false
  28. if ls_val = is_mark then ls_val = ''
  29. as_val = ls_val
  30. return true
  31. end function
  32. // 写数值:double 存、double 读,别跨整数/小数
  33. public function boolean of_putNum (string as_key, double as_val);
  34. if not ib_open then return false
  35. return iuo_kv.Set(as_key, as_val)
  36. end function
  37. // 读数值:失败不改写 as_val
  38. public function boolean of_getNum (string as_key, ref double as_val);
  39. double ld_v
  40. boolean lb_ok
  41. if not ib_open then return false
  42. ld_v = 0
  43. lb_ok = iuo_kv.Get(as_key, ld_v)
  44. if not lb_ok then return false
  45. as_val = ld_v
  46. return true
  47. end function
  48. // 自增序号:单线程计数器,返回新值;失败返回 -1
  49. public function longlong of_nextSeq (string as_key);
  50. longlong lll_v
  51. boolean lb_ok
  52. if not ib_open then return -1
  53. lll_v = 0
  54. lb_ok = iuo_kv.Get(as_key, lll_v)
  55. if not lb_ok then lll_v = 0
  56. lll_v = lll_v + 1
  57. if not iuo_kv.Set(as_key, lll_v) then return -1
  58. return lll_v
  59. end function
  60. // 键清单:逗号拼接,便于打日志
  61. public function string of_keys ();
  62. string ls_keys[], ls_out
  63. long ll_n, ll_i
  64. if not ib_open then return ''
  65. ll_n = iuo_kv.GetKeyList(ls_keys)
  66. ls_out = ''
  67. FOR ll_i = 1 TO ll_n
  68.         if ll_i > 1 then ls_out = ls_out + ','
  69.         ls_out = ls_out + ls_keys[ll_i]
  70. NEXT
  71. return ls_out
  72. end function
  73. // 删键:不信 Remove 的返回值,用 Get 复核;真的没了才返回 true
  74. public function boolean of_drop (string as_key);
  75. string ls_v
  76. boolean lb_ok
  77. if not ib_open then return false
  78. lb_ok = iuo_kv.Remove(as_key)
  79. ls_v = ''
  80. if iuo_kv.Get(as_key, ls_v) then return false
  81. return true
  82. end function
  83. // 整库导出 JSON(调试、体检用)
  84. public function string of_export ();
  85. uo_json lj
  86. boolean lb_ok
  87. string ls_out
  88. if not ib_open then return ''
  89. lj = create uo_json
  90. lb_ok = iuo_kv.Get(lj)
  91. if lb_ok then
  92.         ls_out = lj.ToString(false)
  93. else
  94.         ls_out = ''
  95. end if
  96. destroy lj
  97. return ls_out
  98. end function
  99. // 关库
  100. public function string of_close ();
  101. if IsValid(iuo_kv) then
  102.         iuo_kv.Close()
  103.         destroy iuo_kv
  104. end if
  105. ib_open = false
  106. return 'closed'
  107. end function
  108. // 自检:跑一遍上面所有方法并返回读数
  109. public function string of_run ();
  110. string ls_out, ls_v
  111. double ld_v
  112. longlong lll_a, lll_b
  113. boolean lb_ok
  114. uo_kv lk_clr
  115. // 先用一个临时实例把库删掉,保证每次自检从空库开始、读数可复现
  116. lk_clr = create uo_kv
  117. lk_clr.DeleteDB('kv95_art.db')
  118. destroy lk_clr
  119. ib_open = of_open('kv95_art.db')
  120. ls_out = 'open=' + String(ib_open)
  121. lb_ok = of_putText('title', Char(35746) + Char(21333))   // 存「订单」
  122. ls_out = ls_out + ' put_cn=' + String(lb_ok)
  123. lb_ok = of_getText('title', ls_v)
  124. ls_out = ls_out + ' get_cn=' + String(lb_ok) + '[' + ls_v + ']'
  125. lb_ok = of_putText('blank', '')
  126. ls_v = 'SENT'
  127. lb_ok = of_getText('blank', ls_v)
  128. ls_out = ls_out + ' blank=' + String(lb_ok) + '[' + ls_v + ']' + String(Len(ls_v))
  129. of_putNum('rate', 0.875)
  130. ld_v = -1
  131. lb_ok = of_getNum('rate', ld_v)
  132. ls_out = ls_out + ' num=' + String(lb_ok) + '[' + String(ld_v) + ']'
  133. lll_a = of_nextSeq('seq')
  134. lll_b = of_nextSeq('seq')
  135. ls_out = ls_out + ' seq=' + String(lll_a) + '/' + String(lll_b)
  136. ls_out = ls_out + ' keys=[' + of_keys() + ']'
  137. lb_ok = of_drop('rate')
  138. ls_out = ls_out + ' drop=' + String(lb_ok) + ' keys2=[' + of_keys() + ']'
  139. ls_out = ls_out + ' json_len=' + String(Len(of_export()))
  140. of_close()
  141. return ls_out
  142. end function
复制代码

调用(窗口按钮事件里即可):
  1. // 声明并创建封装对象
  2. nvo_kv_cache lnv
  3. string ls_ret
  4. lnv = create nvo_kv_cache
  5. // 一次跑完所有方法并把读数返回
  6. ls_ret = lnv.of_run()
  7. destroy lnv
  8. MessageBox('uo_kv 封装自检', ls_ret)
复制代码

实机跑出来的自检读数(PB10 与 PB12.5 逐字一致,四轮复跑数值完全相同):
  1. open=true put_cn=true get_cn=true[订单] blank=true[]0 num=true[0.875] seq=1/2 keys=[blank,rate,seq,title] drop=true keys2=[blank,seq,title] json_len=40
复制代码

逐段对读:

读数含义
open=true库目录建好并打开
put_cn=true / get_cn=true「订单」中文值写读闭环
blank=true「」0空串值经哨兵后还原成 0 长度,坑被挡住了
num=true「0.875」double 写读闭环,没有被转成字符串
seq=1/2计数器连跑两次得 1、2(自检前清了库,所以可复现)
keys=「blank,rate,seq,title」键按字节序排列,不是插入序
drop=true key2=「blank,seq,title」删键成功,rate 从清单里消失——of_drop 的返回值是「真的没了」
json_len=40整库导出的 JSON 长度(内容随键值而定)


这里要特别说一句 of_drop 的写法:函数里先把 Remove 的返回值丢掉(存进 lb_ok 但不用它判断),删完再 Get 一次——Get 还读得到说明没删掉,返回 false;读不到才返回 true。原因就是第十一节实测的那条:Remove 删不存在的键也返回 true,这个返回值不能用来判成败。

十五、坑清单(16 条,全是实机踩出来的)

#坑正确做法
1把 dbName 当文件,用 FileLength 量大小恒 -1它是目录,备份拷目录
2以为 Open 返回 true 就代表密码对打开后读哨兵键验证
3跨类型读:int 值用 double 读 false写什么类型就读什么类型
4数值用 string 读,返回 true 但给 * / 空串 / 乱码存字符串,别指望自动转
5Get 有 12 个重载,没有 boolean / uint / ulongboolean 存 1/0;uint 只存小值;ulong 大值别用
6Set(key, true) 返回 false,键都没建存 int 1 或 string 'Y'
7uint 4000000000 读回 10240uint 被截成 16 位
8ulong 3000000000 读进 longlong 变负数用 any 接,或干脆不用 ulong
9空串值 Set 成功但 Get 失败用哨兵串(本篇 of_putText)
10Remove 删不存在的键也返回 true删完 Get 复核
11无参 DeleteDB() 删掉当前库,实例随后全 false用带库名的版本;删完重新 Open
12Close 后还继续 Set,静默 false维护 ib_open 状态位
13想按插入顺序取键键名加零填充序号,排序是字节序
14GetKeyList 传定长数组 string ls「2」 → C0052必须传动态数组 string ls「」
15以为 Get(any) 万能any 只忠实还原,不替你转换;取值前先判类型
16哨兵串里用了 ~(如 '~EMPTY~')→ C0037 Unclosed quoted string,报错行号还是错的~ 是 PB 的转义符;哨兵别用 ~,改用 #EMPTY# 这类全 ASCII 标记


十六、速查表

想做的事写法
开库(不加密)lk.Open('缓存目录', '')
开库(加密)lk.Open('缓存目录', '口令'),口令丢了数据就没了
判库是否可用Open 后读一个哨兵键,读得到才算通
写字符串lk.Set('k', 'v'),空串先换哨兵
读字符串lk.Get('k', ls),false = 键不存在或类型不符
写/读数值存 double 读 double;整数存 int/long,按同族读
计数器读 longlong → +1 → 写回(单线程)
判键是否存在lk.Get('k', 任意同类型变量) 的返回值
键总数lk.GetKeyList(动态数组) 的返回值
删键lk.Remove('k'),再用 Get 复核
清库lk.DeleteDB('库名'),下次 Open 自动重建空库
整库导出uo_json lj + lk.Get(lj) + lj.ToString(true)


十七、PB12.5 差异

uo_kv 本身的行为两版一致,没有差异。 17 个探针/示例对象在 PB10 与 PB12.5 两条轨上分别导入、全量编译、运行,所有断言读数逐条对齐。要把话说严谨,两处非 uo_kv 行为的表面差异也列出来,免得读者对不上号:

现象PB10 读数PB12.5 读数根因
decimal 值经 String() 转出来dec=12.75000000000000000dec=12.75PB 自身 String(decimal) 默认格式:PB10 展开 18 位小数,PB12.5 用最短表示。不是 uo_kv 存的精度不同——Get 回 decimal 变量后两版数值完全相等
1000 次 Get 的 Cpu() 读数get1000_ms=15get1000_ms=0计时器精度/取整差异,与库无关;比对性能请看量级(几十毫秒),不要比个位数


还有一处与差异无关但会咬人的部署细节:PBIDEA 的 .pbl 分 PB10 格式与 PB12.5 格式两种,PB10 格式的库在 PB12.5 下会报 C0101 ... must be converted。部署时按目标大版本选对应的 websuite.pbl,别混用。

十八、实机验证情况

项目PB10PB12.5
导入(命令行导入,GBK/CRLF/无 BOM 源)17 个对象全部 0 错17 个对象全部 0 错
全量编译(build rebuild --type full)0 错误0 错误
运行(PBVM 跑 .pbtest)17 组全部 PASS17 组全部 PASS
断言读数比对基准与 PB10 逐条对齐(仅十七节列出的两处显示格式差异)
静态检查源码规范检查 0 错 0 提醒 + 编码检查 bad=0同(同一份源码)


覆盖到的行为:开库/关库/重开、目录落盘形态、12 个类型重载逐项往返、跨类型读矩阵、空键/长键/特殊字符键/中文键、空串值、NULL 值、boolean/uint/ulong 边界、密码(空/错/对)、GetKeyList 扩容与排序、Remove 返回值、DeleteDB 两种、多实例共享、未开库降级、坏路径、整库 JSON 导出、64000/80000 字符大值、1000 次读写性能、GBK 落盘编码。

未实测的部分(如实标注):①真·跨线程(SharedObjectRegister + 多线程并发写同一库)未做压测,本文只对同进程多实例给结论;②网络盘/共享目录上的库目录行为未测;③单值大小上限未做二分探测(只证到 80000 字符可用)。这三项不构成使用建议的一部分,遇到请自行验证。

关于本文的代码:正文所有 powerbuilder 代码块均在 PB10 与 PB12.5 实机上编译 0 错误、运行 PASS;nvo_kv_cache 的完整对象已随文打包成 PB10 兼容 PBL(见本帖附件),可导入后直接调用 of_run() 自检。

十九、最后三句


  • uo_kv 的价值在「不用配环境就能持久化」,别拿它当数据库用,它没有 SQL、没有事务、没有索引查询。
  • 类型是这个组件的全部脾气:写什么读什么,跨族就是 false,跨到字符串还可能给你垃圾。
  • 返回值不能全信:Open 不代表密码对,Remove 不代表删掉了,Get 成功不代表值是好的——三个地方各加一次复核,省下的排障时间远超这点代码。

共享共进共赢
Sharing And Win-win Results
SYBASEBBS - 免责申明1、欢迎访问“SYBASEBBS.COM”,本文内容及相关资源来源于网络,版权归版权方所有!本站原创内容版权归本站所有,请勿转载!
2、本文内容仅代表作者观点,不代表本站立场,作者自负,本站资源仅供学习研究,请勿非法使用,否则后果自负!请下载后24小时内删除!
3、本文内容,包括但不限于源码、文字、图片等,仅供参考。本站不对其安全性,正确性等作出保证。但本站会尽量审核会员发表的内容。
4、如本帖侵犯到任何版权问题,请立即告知本站 ,本站将及时删除并致以最深的歉意!客服邮箱:admin@sybasebbs.com
 楼主| 发表于 9 小时前 | 显示全部楼层
本帖最后由 pbai 于 2026-11-12 06:27 编辑

PB10 兼容版 PBL 交付(kv95 · nvo_kv_cache95)

附件: kv95_pb10.zip (10.62 KB, 下载次数: 1)

包内容(6 条目)
- kv95_pb10.pbl —— PB10 编译通过(内含 nvo_kv_cache95 一个对象,封装 uo_kv 做键值缓存)
- kv95_pb10.pbt —— 配套工程(liblist 含依赖库文件名说明,见下)
- src/nvo_kv_cache95.sru —— 源码(GBK 编码,可 Import)
- tests/t_kv_cache95.pbtest —— 自验证脚本
- evidence/ —— PB10 与 PB12.5 两版实测输出留档(逐字一致)

环境要求
- PowerBuilder 10 及以上(本包按 PB10 基准写,PB12.5 亦实测通过);Windows
- 依赖 PBIDEA:nvo_kv_cache95 内部使用 uo_kv,运行/编译需把 PBIDEA 的 websuite.pbl 挂进 liblist(包内 PBT 只保留文件名 + 注释,请按你的 PBIDEA 安装位置补全路径)
- 无数据库连接依赖:uo_kv 是嵌入式键值库(LevelDB 形态),dbName 是目录不是文件,落盘位置即运行目录下的 kv95_art.db 目录

操作步骤
1. 解压 ZIP 到本地任意目录;
2. 把 kv95_pb10.pbl 加入你的工程 liblist,同时挂上 PBIDEA websuite.pbl;
3. 全量 Rebuild 应 0 错误;
4. 源码方式:在目标 PBL 上 Import src/nvo_kv_cache95.sru(GBK 编码);
5. 验证:运行 tests/t_kv_cache95.pbtest 的 test_kv_cache95(),会在运行目录生成 g95_kv_cache95.txt,预期读数:
   open=true put_cn=true get_cn=true[订单] blank=true[]0 num=true[0.875] seq=1/2 keys=[blank,rate,seq,title] drop=true keys2=[blank,seq,title] json_len=40
   末行 PASS。脚本开头的 of_run 会先用临时实例 DeleteDB 清掉 kv95_art.db,读数可复现。

方法清单(11 个 of_ 方法)
of_open(带口令)/ of_close / of_putText / of_getText / of_putNum / of_getNum / of_nextSeq / of_keys / of_drop(Get 复核式删除)/ of_export(Get(uo_json) 整库导出)/ of_run(演示入口)

实机验证情况
- 双版本全量重建 0 错误(PB10 口径与 PB12.5 口径各一遍);
- 独立运行器实跑验证脚本,两版输出逐字一致(见包内 evidence/);
- 未覆盖:口令错误的库跨实例续用、超长值(>32KB)边界,正文已如实标注。

要点回顾见首帖正文;最容易踩的三条:dbName 是目录不是文件、boolean 存不进(Set 直接 false 且键不建)、错口令 Open=true 只有 Get 才 false。
共享共进共赢
Sharing And Win-win Results
您需要登录后才可以回帖 登录 | 站点注册

本版积分规则

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

Mail To:Admin@SybaseBbs.com

客服微信:18669893686
挂谷猜想 · 探索

Archiver|PowerBuilder(PB)BBS社区 ( 鲁ICP备2021027222号-1 )

GMT+8, 2026-11-12 15:33 , Processed in 0.017262 second(s), 8 queries , MemCached On.

Powered by Discuz! X5.0

© 2001-2026 Discuz! Team.

快速回复 返回顶部 返回列表