ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

VB6调用SQLite实战:litex_sqlite封装选型、避坑与性能调优

VB6调用SQLite实战:litex_sqlite封装选型、避坑与性能调优 简介一份专门面向 Visual Basic 6 开发者的 SQLite 嵌入式数据库封装包litex_sqlite解决 VB6 程序在无独立服务器环境下的本地数据存储与管理需求尤其适合桌面型 MIS、进销存、工具软件等对性能有较高要求的应用。压缩包共 134 个文件约 1.5MB包含 C/C 核心源码、编译好的 DLL/Lib 库、VB6 的模块与窗体文件、工程工作区与解决方案文件、注册脚本、测试示例、AutoIt 自动化脚本、头文件接口定义以及 SQLite 3.3.5 配套版本还配有详细使用说明既能直接集成进 VB6 工程也可按需重新编译。已有 766 人学习下载是 VB6 开发者接入 SQLite 的常用参考特别适合需要快速落地的中小型项目。借助压缩包内的示例与文档可完整了解从环境注册、工程引用到调用 API 执行建表、插入、查询、更新等操作的流程同时解决 DLL 注册、库依赖等常见问题。头文件与源代码也便于深入理解封装机制进行二次定制从而让 VB6 程序直接获得 SQLite 的稳定存储能力。1. 还在维护VB6老系统的兄弟SQLite这条路怎么选很多人以为VB6是上个世纪的古董可进销存、工控上位机、老MIS系统里还跑着大量VB6程序。这些程序要加本地存储Access文件几十MB后性能直线下降还容易损坏SQL Server Server Compact早已停止维护装完整版SQL Server又太重。单文件、零配置的SQLite成了最现实的选择。litex_sqlite就是“sqlite支持vb6的版本”这条路上常用的一套封装它把SQLite的C接口翻译成VB6能直接调用的函数老工程不用大改就能落库。这篇文章写给还在维护这类系统的人接入路线怎么选、最小代码怎么跑通、参数怎么设、哪些坑绕不开。新项目就别折腾VB6了下面这些经验只服务存量系统。2. 为什么VB6跑SQLite这么别扭三条路线与litex_sqlite的选型逻辑VB6官方没有提供SQLite的ActiveX控件也没有托管接口SQLite官方只发布一组C语言API的DLL。这个前提决定了VB6接入SQLite必须走一层“翻译”。常见做法有三条ADO配ODBC驱动、直接Declare sqlite3.dll、用litex_sqlite这类现成封装。先讲清楚三条路的差别后面踩坑时你才知道问题到底出在哪一层。2.1 ADOODBC这条最省事的路为什么我最后没走先抛结论ADOODBC在VB6里操作SQLite写代码确实像连Access一样舒服。不需要理解SQLite的C接口打开Connection拼SQLRecordset查出来老VB程序员闭着眼都能写。我第一次做SQLite改造就是这么干的开发两天就完事。部署时才开始翻车。SQLite的ODBC驱动不是Windows自带的客户端每台机器都要装而且驱动的位数和版本得跟系统里其他组件对齐。老运维机环境五花八门有的装过旧版驱动有的装的是另一个分支的SQLite ODBC连接串参数不兼容开发机上跑得好好的程序拷到车间机器上报“找不到数据源”。更别扭的是ODBC驱动对SQLite新特性的支持滞后想开WAL、想设busy_timeout驱动层面经常不暴露这些参数最后只能退回去用默认的journal模式。这条路适合写一次性脚本、能接受每台机器装驱动的场景我做产品化分发之后彻底放弃。2.2 直接Declare sqlite3.dll接口能用但处处是暗坑第二条路是硬刚C接口。SQLite官方发布的sqlite3.dll导出函数齐全VB6用Declare Function声明之后确实能调。但SQLite对外API是给C/C用的一堆指针、回调、编码转换。sqlite3_open_v2返回一个数据库句柄查询要走prepare、step、column这一串操作VB6处理指针本来就别扭稍不留神就是内存泄漏。更痛苦的是编码。SQLite内部存UTF-8VB6的String是UnicodeWindows的ANSI编码又是中间层。直接传一个中文路径给sqlite3_open顶层是VB6的BSTR底层DLL按ANSI收转成UTF-8时只要封装没处理好路径就错了读出来的sqlite3_column_text返回的是UTF-8字节还得自己做多字节转宽字符。这套转换逻辑写一次能跑通但每个工程复制一遍出问题排查费大量时间。属于“能用但别碰”的路线只适合拿来做学习实验。2.3 litex_sqlite这类封装的价值把C API收成了VB6函数常见的做法是直接用社区里已经封装好的模块。litex_sqlite这类库本质上是把2.2小节你自己要写的那层胶水代码写好Declare sqlite3.dll的导出函数再用VB6代码包一层把指针、回调、UTF-8转换全部收进内部对外暴露Open、Execute、Query这类符合VB6习惯的函数。传字符串进去拿字符串出来中间的黑匣子由封装负责。我选型时一般看四个维度部署成本——纯DLL还是要注册OCX接口风格——函数式还是对象式更新频率——封装有没有跟着SQLite核心版本走排查深度——出错时给不给SQLite原生错误信息。在VB6这个生态里litex_sqlite被讨论得多是因为它的接口做得足够薄接近直接操作SQLite学过的SQL知识不浪费排查时还能往下追到SQLite层。接入路线客户端部署编码与指针查询接口适用场景ADO ODBC每台机装驱动驱动处理Recordset一次性脚本、内部工具直接Declare sqlite3.dll只带DLL自己处理指针硬调学习验证工程慎用litex_sqlite封装只带DLL和模块封装处理函数式存量VB6产品改造结论很简单产品化、要走多台机器就用封装路线。下面几章全部按litex_sqlite的操作方式来写你可以对照自己手头那套封装微调函数名。3. 把litex_sqlite装进工程DLL放置、注册与最小可用代码从这一章开始动手。老工程改造顺序是先把封装库放进工程目录再写模块级声明最后跑通最小用例。这步走通后面所有查询、事务、排错都在这个骨架上加。3.1 先搞清楚它是什么形态标准DLL不需要注册OCX才需要拿到litex_sqlite压缩包第一步看里面是DLL还是OCX或者是纯.bas源码模块。如果是标准DLL走Declare路线不需要regsvr32注册放进工程目录、在标准模块里声明函数就能用如果是OCX控件才需要regsvr32去注册工程里还要勾选部件引用。判断方法很简单看包里有没有.bas声明文件或者有没有说明文档提到“regsvr32”。我一般把DLL放在App.Path下的bin目录跟EXE一起分发不往System32里塞。原因是老机器上System32权限控制不一而且不同版本的DLL混放容易互相覆盖放在程序自己的目录里最省心。如果工程要打包成安装包记得把bin目录加进去安装时用相对路径定位。还有一个细节VB6的IDE里调试时App.Path是VB6.exe的目录不是工程目录所以调试阶段DLL要么放工程目录要么把路径写成固定绝对路径不然会报“无法加载DLL”。3.2 最小可用代码打开、建表、插入一条数据下面这段是标准模块里的声明和测试过程。litex_sqlite的函数名在不同版本里可能有出入以你手上的.bas声明文件为准调用逻辑是一致的。 模块级函数名和参数以你手上的.bas声明为准 Private Declare Function litex_open Lib litex_sqlite.dll _ (ByVal dbName As String) As Long Private Declare Function litex_exec Lib litex_sqlite.dll _ (ByVal dbHandle As Long, ByVal sql As String) As Long Private Declare Function litex_close Lib litex_sqlite.dll _ (ByVal dbHandle As Long) As Long Private Declare Function litex_errmsg Lib litex_sqlite.dll _ (ByVal dbHandle As Long) As String Sub TestInit() Dim hDB As Long Dim rc As Long Dim dbFile As String dbFile App.Path \data\test.db 确保data目录存在SQLite不会自动创建父目录 On Error Resume Next MkDir App.Path \data On Error GoTo 0 hDB litex_open(dbFile) If hDB 0 Then MsgBox 打开数据库失败 Exit Sub End If rc litex_exec(hDB, CREATE TABLE IF NOT EXISTS stock( _ id INTEGER PRIMARY KEY AUTOINCREMENT, _ name TEXT NOT NULL, _ qty INTEGER DEFAULT 0)) If rc 0 Then MsgBox 建表失败: litex_errmsg(hDB) litex_close hDB Exit Sub End If rc litex_exec(hDB, INSERT INTO stock(name, qty) VALUES(螺丝钉, 200)) If rc 0 Then MsgBox 写入失败: litex_errmsg(hDB) End If litex_close hDB End Sub这段代码做了四件事打开库、建表、插入、关闭。参数上注意几点第一litex_open返回0表示打开失败SQLite句柄从1开始编号所以判断用“ 0”而不是“ 0”第二返回码rc遵循SQLite约定0表示成功非0是错误码具体含义用litex_errmsg取文本最直观第三CREATE TABLE IF NOT EXISTS是幂等操作重复执行不会报错适合程序每次启动都跑一遍建表语句第四MkDir外面包了On Error因为目录已存在时会报错这里故意忽略。3.3 查询与update语句结果集遍历与影响行数插入跑通后查询和更新是日常主力。litex_sqlite这类封装一般提供独立的查询句柄遍历方式和Recordset不同看下面这段Private Declare Function litex_query Lib litex_sqlite.dll _ (ByVal dbHandle As Long, ByVal sql As String) As Long Private Declare Function litex_next Lib litex_sqlite.dll _ (ByVal rsHandle As Long) As Long Private Declare Function litex_fieldText Lib litex_sqlite.dll _ (ByVal rsHandle As Long, ByVal colIndex As Long) As String Sub QueryStock() Dim hDB As Long Dim rs As Long Dim id As String, name As String, qty As String hDB litex_open(App.Path \data\test.db) If hDB 0 Then Exit Sub rs litex_query(hDB, SELECT id, name, qty FROM stock WHERE qty 0 ORDER BY id) If rs 0 Then litex_close hDB Exit Sub End If Do While litex_next(rs) 0 id litex_fieldText(rs, 0) name litex_fieldText(rs, 1) qty litex_fieldText(rs, 2) Debug.Print id, name, qty Loop litex_close hDB End Sublitex_next返回0表示还有下一行返回非0表示遍历结束这个约定和ODBC的SQLFetch相反第一次用容易写反循环条件。字段索引从0开始和SQL语句里的列顺序一致。前面说过格式化输出之前最好先做数值处理这里只用Debug.Print是为了看清楚原始值。update语句在SQLite里的坑不多核心是加WHERE条件否则全表更新 常见错误不加WHERE会把全表qty改成0 rc litex_exec(hDB, UPDATE stock SET qty 0) 正确做法 rc litex_exec(hDB, UPDATE stock SET qty qty - 10 WHERE name 螺丝钉) If rc 0 Then Debug.Print 更新成功 End If如果封装提供受影响行数函数更新后取一下行数能确认条件是否真的命中了数据没有的话更新后再SELECT一次比对更保险。SQLite的update语句本身不复杂真正复杂的是“怎么判断更新了几行”——很多封装不暴露sqlite3_changes我一般会在更新前后各查一次目标条件的数据用数量差确认。4. 三个必调参数与数据安全配置并发、事务、加密单机程序也要面对三个绕不开的问题别的进程同时打开同一个库、循环写入慢、以及“sqlite数据库文件能否加密”这个几乎每个客户都会问的事。这些问题不解决程序在客户机器上跑几天就会暴露。4.1 busy timeout和WAL把database is locked压下去VB6程序里最常见的锁错误是“database is locked”现象是连续快速写入时报错或者程序开着多个窗口共用同一个数据库句柄时偶发。原因是SQLite默认的journal_mode是DELETE模式写事务开始时拿EXCLUSIVE锁另一个连接只要没等到锁就会立刻报错。两个参数能解决大部分场景直接通过litex_exec执行PRAGMA 设置忙等待超时单位毫秒 rc litex_exec(hDB, PRAGMA busy_timeout 3000) 开启WAL日志模式读写并发能力大幅提升 rc litex_exec(hDB, PRAGMA journal_mode WAL)busy_timeout的意思是拿不到锁时最多等3秒而不是立刻报错。这个值对单机程序设1000到3000都合理设太大用户在界面卡住设太小等于没设。WAL模式把写操作改成追加到-wal文件读操作不再被写锁阻塞对VB6这种多窗口操作同一个库的场景改善明显。注意两个点journal_mode是持久化设置执行一次之后同一个数据库文件以后打开都保持WAL模式但WAL会生成.db-wal和.db-shm两个临时文件备份数据库时不能只拷.db文件否则丢数据。老运维拷库的习惯得提前跟客户说清楚。如果客户环境的老旧杀毒软件对多文件特别敏感可以考虑退回DELETE模式只设busy_timeout。4.2 循环写入必须开事务速度差一个量级VB6里逐条INSERT每条自带一个隐式事务等于每插入一条就做一次磁盘同步1万条数据可能要跑几十秒。包一层显式事务同一批提交速度快一个量级这是SQLite性能优化里性价比最高的一项Dim i As Long Dim rc As Long Dim hDB As Long hDB litex_open(App.Path \data\test.db) rc litex_exec(hDB, BEGIN IMMEDIATE) If rc 0 Then MsgBox 事务开启失败 litex_close hDB Exit Sub End If For i 1 To 10000 rc litex_exec(hDB, INSERT INTO stock(name, qty) VALUES(批量, i )) If rc 0 Then 失败就回滚不留半截数据 litex_exec hDB, ROLLBACK MsgBox 第 i 条写入失败: litex_errmsg(hDB) litex_close hDB Exit Sub End If Next i rc litex_exec(hDB, COMMIT) If rc 0 Then litex_exec hDB, ROLLBACK End If litex_close hDBBEGIN IMMEDIATE和普通BEGIN的区别在于普通BEGIN是DEFERRED事务第一条写语句执行时才拿写锁中间如果别的连接已经写了这个事务会拿不到锁BEGIN IMMEDIATE在事务开始时就拿写锁更早暴露锁冲突反而容易处理。循环里每一条都得检查rc你永远不知道哪一条会遇到约束冲突事务回滚是唯一后悔药。写入密集的场景把批量值放进Text文件逐行读再插入比拼SQL字符串更可控。4.3 sqlite数据库文件能否加密VB6下的现实回答这个问题几乎每个客户都会问。SQLite官方不提供免费的文件级加密商业方案是SQLite Encryption Extension但它是C接口VB6直接调用很麻烦license费用也不低。开源方案SQLCipher把整个库加密但编译出来的DLL是给C/C用的VB6声明调用那套在它身上基本走不通而且litex_sqlite这类封装底层连的是标准sqlite3.dll换掉DLL后封装就废了。我的做法是分两层处理。第一层是敏感字段加密在VB6里用系统自带的CAPICOM或.NET写好的加密DLL把身份证号、手机号这类字段单独加密成Base64字符串再入库查询时解密用其他字段明文存储。第二层是整库文件保护数据库文件放在用户数据目录用Windows的EFS加密文件夹这个不需要改一行代码。结论放在这里如果你需要的是绕开商用授权、内置的公开标准加密VB6SQLite这个组合没有完美答案。真想整库加密就得接受SQLCipher重写访问层或者升级到.NET。老项目维护阶段字段级加密足够应对绝大多数审计需求。5. litex_sqlite排错与避坑五条血泪经验这一章写的是我把litex_sqlite用到生产环境后遇到的最有代表性的五个问题。每一条都按现象、原因、解决的顺序写排查时可以对照。5.1 现象程序启动报错“找不到DLL入口点”这个错误出现的场景很典型开发机Win7 32位一切正常拷贝到新配的Win10电脑上程序起来就报“无法定位程序输入点……于动态链接库sqlite3.dll上”。原因几乎都是64位DLL混入。VB6的EXE是32位进程它只能加载32位的sqlite3.dll而新机器上可能装了64位版本的DLL或者安装包把64位版本覆盖了开发用的32位版本。解决检查程序目录下sqlite3.dll的位数用Visual Studio自带的dumpbin或者简单的文件属性查看。注意发版前把32位DLL和64位DLL分开命名sqlite3.dll只放32位版64位版放到sqlite3_x64.dll这种独立文件里绝不能用同名文件覆盖。这个错浪费了我一下午后来在构建脚本里加了一步位数检查再没犯过。5.2 现象数据库文件路径带中文就打不开客户机器用户名是“张三”程序的数据目录在C:\Users\张三\AppData\Roaming\MyApp运行后报“unable to open database file”。原因不是权限是编码。VB6的String传给DLL时默认按ANSI其实就是本机代码页转成字节而SQLite的接口把传入路径当UTF-8解析。中文路径在ANSI和UTF-8之间来回转换只要有一层不对就匹配不上。解决思路有两个。第一个是给数据库文件换到纯英文路径简单粗暴但客户目录不受你控制第二个是用Windows的GetShortPathName把长路径转成8.3短路径短路径里不含中文再传给litex_open。我用的就是第二种而且把转换封装成一个独立函数Private Declare Function GetShortPathName Lib kernel32 _ Alias GetShortPathNameW _ (ByVal lpszLongPath As Long, ByVal lpszShortPath As Long, _ ByVal cchBuffer As Long) As Long Function ShortPath(ByVal longPath As String) As String Dim buf As String Dim len As Long buf String(260, 0) len GetShortPathName(StrPtr(longPath), StrPtr(buf), 260) If len 0 Then ShortPath Left$(buf, len) Else ShortPath longPath End If End Function注意GetShortPathName用的是W版本参数传StrPtr取指针否则长路径里有中文一样转不对。这个函数返回的短路径形如C:\Users\ZHANS~1\AppData...虽然难看了点但兼容性最好。如果litex_sqlite封装本身处理了Unicode路径这步可以跳过但我在几个版本里都遇到问题干脆统一走短路径眼不见心不烦。5.3 现象读出来的中文全是乱码写入的时候一切正常SELECT查出来Debug.Print都是“鏉犱笣”典型的UTF-8字节被按ANSI解码了。原因SQLite里存的是UTF-8litex_exec把SQL传进去时封装做了转换但查询结果字段文本可能没转或者转反了。解决确认你手上的litex_sqlite版本是否处理了字段编码。没有处理的话查询结果拿到UTF-8字节数组用StrConv转Private Declare Function litex_fieldBlob Lib litex_sqlite.dll _ (ByVal rsHandle As Long, ByVal colIndex As Long, _ ByRef bytes() As Byte) As Long Function FieldUTF8Text(rs As Long, col As Long) As String Dim bytes() As Byte Dim len As Long len litex_fieldBlob(rs, col, bytes) If len 0 Then FieldUTF8Text StrConv(bytes, vbUnicode) End If End FunctionStrConv配合vbUnicode是把UTF-8字节数组转成VB6 Unicode字符串的标准做法。但这个方法要小心如果字段本身是纯ASCII转出来没有异样一旦混入中文转错编码的乱码是不可逆的数据本身没坏坏的是显示层。我后来把查询字段统一走这个转换函数不再直接用fieldText乱码问题绝迹。5.4 现象程序退出后数据库文件还被占用无法拷贝备份VB6程序关闭了主窗口但备份数据库时报“文件正在被另一个进程使用”。原因几乎都是同一个某个打开的数据库句柄没有关。VB6不像.NET有析构函数全局变量里的hDB不会因为窗体Unload自动释放。解决在所有退出路径上补litex_close。特别是用了错误处理On Error Resume Next的地方很容易跳过关闭代码。我的习惯是写一个统一的关闭函数在Form_Unload、App.Terminate、甚至Err.Raise之后都调一遍Public Sub CloseDB(hDB As Long) If hDB 0 Then litex_close hDB hDB 0 End If End Sub关键点是关闭后把句柄置0防止重复关闭。另外如果程序里开了多个查询结果集rs也要逐个释放。这也是“玄学”味道最浓的一条看不到报错就是文件锁着找到最后发现是最早测试时漏了close。建议在程序里保留一个调试菜单能直接查看当前有几个连接句柄排查起来省事很多。5.5 现象新电脑报错“缺少运行时”或“控件未注册”把程序拷到新装的Windows电脑上双击EXE报“运行时错误339”或“组件未正确注册”。原因不是litex_sqlite本身而是VB6程序依赖的VB6运行时库MSVBVM60.dll在新系统里不一定预装。Windows 10之后微软不再默认带VB6运行时新装的精简版系统尤其常见。解决发布时把VB6运行库VB6运行库下载包里那套一起分发安装程序里加一步静默安装或者直接把MSVBVM60.dll放到程序目录。更隐蔽的是litex_sqlite依赖的VC运行时如果封装DLL是用VC2015编译的目标机器还得有对应的vcruntime140.dll。我的习惯是发版前在一台干净虚拟机里测安装包保证从裸系统到程序可用不依赖客户机器预装任何东西。这种坑不是代码问题是分发打包的问题但客户只会归咎于“程序装不上”。6. 进阶封装一个通用查询模块并用DB Browser验证数据如果你已经跑通上面这些最后一步是把访问层收敛到一个公共模块里让业务代码只拼SQL字符串不直接碰句柄。最简单的封装就是两个函数一个执行不返回结果的SQL一个执行查询并返回结果集句柄。 通用执行函数 Public Function DBExec(dbPath As String, sql As String) As Long Dim hDB As Long hDB litex_open(ShortPath(dbPath)) If hDB 0 Then DBExec -1 Exit Function End If DBExec litex_exec(hDB, sql) litex_close hDB End Function 通用查询函数调用方拿到rs后自己遍历用完必须释放 Public Function DBQuery(dbPath As String, sql As String) As Long Dim hDB As Long hDB litex_open(ShortPath(dbPath)) If hDB 0 Then DBQuery 0 Exit Function End If DBQuery litex_query(hDB, sql) End Function这样业务层写起来很干净DBExec UPDATE stock SET qty0 WHERE id5DBQuery取数据后逐行处理。缺点是一个查询开一次连接但对VB6老系统来说换来的是业务代码里没有一句Declare后期找人维护时门槛低很多。实测下来这种方式处理几千行数据毫无压力日常单机业务足够。写完代码验证永远用DB Browser for SQLite。这个工具是SQLite生态里最常用的图形化客户端可以打开你生成的.db文件直接看表结构、浏览数据、执行任意SQL还能检查WAL模式是否生效。程序生成数据后我习惯用DB Browser打开看一眼确认表存在、字段值对、没有多余的临时表再交给客户。数据库文件用什么打开这个问题以后也不会再纠结——它就是SQLite的官方社区标配免费Windows和Linux都能跑。说到教训我最想重复的还是那句老系统改造稳定压倒一切能少改就少改。用litex_sqlite把Access换成SQLite的那次改造我前前后后花了三周真正写代码只用四天剩下时间全耗在DLL位数、中文路径和编码乱码这些环境问题上。如果你正在做同样的事从最小用例开始先跑通打开和建表再写业务逻辑DLL和运行库的版本号在发布清单里写清楚别指望客户帮你排查环境问题。希望帮到你。本文还有配套的精品资源点击获取
返回列表