简介这是一份为 Visual Basic 6 开发者准备的 SQLite 集成方案面向需要在桌面应用中实现轻量级本地数据库存储的编程人员。litex_sqlite 封装了嵌入式 SQLite 引擎支持在 VB6 环境中直接执行建表、增删改查等常见操作适合对性能与部署便捷性有要求的单机应用。压缩包共 134 个文件体积约 1.5MB包含 h/cpp 源码头文件、bas/frm 等 VB6 工程文件、dll/lib 运行库以及 LiteX.dsw、LiteX.sln、VBP 等构建配置另有 txt 说明文档与少量测试用 exe、db 示例文件目录划分清晰方便按需引用。包中的 litexSqlite3使用说明.txt 是集成关键文档可指导配置引用、导入库和调用 API通过配合 ADODB 或 DAO 组件可稳定封装 SQL 操作。已有 766 人学习下载适合有一定 VB6 基础、希望为传统桌面程序引入 SQLite 数据管理能力的开发者参考。1. 老VB6项目换数据库为什么绕不开litex_sqlite一个维护了十来年的VB6系统Access数据库文件到2GB上限后频繁报“不是有效的文件名”业务方要求换库但坚决不让改界面。我当时的第一个念头就是迁到sqlite因为它单文件、零服务、部署时拷一个exe加一个db3就能跑。但官方sqlite3.dll给的是纯C接口VB6里要用Declare去声明函数指针、内存释放、UTF-8字符串转换全得自己处理写错一个参数就崩溃根本不适合交给后面接手的同事维护。litex_sqlite这类封装的定位就是把sqlite3.dll包成VB6能直接New出来的COM对象让老程序用几乎不变的方式完成连接、查询和事务。这篇东西就是围绕这条路线把环境部署、增删改查、并发参数和踩坑记录讲清楚适合还在用VB6维护老系统、想替换Access又不想重写整套代码的工程师参考。2. 面向VB6的SQLite接入三条路线与litex_sqlite的定位2.1 三条路线ODBC、裸调sqlite3.dll、litex_sqlite封装到底选哪个网上搜“vb6 sqlite”跳出来的方案大致分三类我分别跑过一遍先说结论最省心的是用封装好的litex_sqlite其次是裸调sqlite3.dll最不建议的是ADOODBC驱动。路线典型写法优点主要问题ADO ODBC驱动配置系统DSN后cn.Open ProviderMSDASQL;...代码改动小ADO写法熟悉官方维护的ODBC驱动难找部署时每台客户端都要配数据源裸调sqlite3.dllDeclare Function sqlite3_open Lib sqlite3.dll ...不依赖第三方DLL直接拷要自己处理sqlite3_stmt指针、sqlite3_finalize中文编码坑多litex_sqlite封装Set cn New LitexConnection类名接近ADO代码直白封装了UTF-8和事务需要在项目里引用DLL资料相对少ADOODBC我最早试过发现SQLite的ODBC驱动要么版本老要么安装包要带一堆注册表项在客户机器上装一次就想放弃。裸调sqlite3.dll也有一段时间跑通了但每次查询后用sqlite3_finalize清理少调一次就内存泄漏调试起来像在补窟窿。litex_sqlite这类封装则是把连接、命令、结果集都做成对象VB6里Set、Dim、For循环的基本功就能上手。它并没有改SQLite本身只是在外面包了一层转换把sqlite3.dll的C接口翻译成VB6认识的对象和Method所以数据库文件仍然能被DB Browser for SQLite直接打开。2.2 部署材料sqlite3.dll、VB6运行库与文件放哪常见做法是exe目录下放三个文件主程序.exe、sqlite3.dll、litex_sqlite.dll。sqlite3.dll是SQLite官方编译的DLLlitex_sqlite.dll是封装层两者缺一不可。封装层如果是ActiveX DLL首次部署时可能需要RegSvr32注册如果封装被设计成免注册的Direct COM调用方式那就不需要注册直接用CreateObject或New引用即可。老机器如果连VB6程序都跑不起来还要先补VB6运行库最简单的判断方法是看msvbvm60.dll在不在System32目录里不在的话用安装包补上。文件作用备注sqlite3.dllSQLite核心引擎必须是32位版本VB6程序是32位进程litex_sqlite.dll把C接口封装成VB6对象与sqlite3.dll版本配套不配套会报入口点错误msvbvm60.dllVB6运行时缺了它程序根本起不来目标.db3业务数据库首次连接时由程序自动创建路径有个技巧不要在Form_Load里用相对路径“sqlite.db”这取决于当前工作目录双击exe和用VB6 IDE运行时的当前目录不一样容易导致程序找不到库文件。我一般固定写成App.Path加上文件名或者用App.Path向上拼一层数据目录避免测试环境正常、发布环境翻车。2.3 最小可用代码连接空库并读取SQLite版本先跑通最小连接确认环境没问题。下面这段代码放到Form的Command按钮里能弹出版本号就说明DLL引用和路径都对了。Dim cn As LitexConnection Dim rs As LitexRecordset Set cn New LitexConnection cn.Open App.Path \test.db3 Set rs cn.Execute(SELECT sqlite_version() AS ver) Debug.Print SQLite version: rs(ver) rs.Close cn.Close Set rs Nothing Set cn Nothing逻辑说明Open的第一个参数是数据库文件的绝对路径这里用App.Path拼出完整路径避免工作目录不同导致找不到文件。Execute执行一条SQL并返回结果集SELECT sqlite_version()是SQLite内置函数用来验证DLL版本和封装是否正常。代码末尾把rs和cn都置为NothingVB6里手动释放对象是好习惯尤其是在Form卸载或程序退出时。参数说明如果你的封装把连接对象不叫LitexConnection或者在打开时要传用户名密码那就以实际引用的类型库为准核心思路不变。出现“未找到类型库”的编译错误就去“工程—引用”里勾选litex_sqlite的类型库勾完后对象名会出现在对象浏览器里照着里面的接口名抄就行不需要自己猜。2.4 冒烟测试在一个临时库上验证写入与读回版本号读出来只是第一步还要验证封装层是否正确处理了写操作和中文。写一段冒烟测试建临时表、插入一条记录、读回总数整个过程不碰业务数据。Dim cn As LitexConnection Set cn New LitexConnection cn.Open App.Path \smoke.db3 cn.Execute CREATE TABLE IF NOT EXISTS t1(id INTEGER PRIMARY KEY, name TEXT) cn.Execute INSERT INTO t1(name) VALUES(冒烟测试) Dim rs As LitexRecordset Set rs cn.Execute SELECT COUNT(*) AS total FROM t1 Debug.Print 记录数: rs(total) rs.Close cn.Close Set rs Nothing Set cn Nothing这段代码验证三件事CREATE TABLE能否执行、INSERT能否写入中文、SELECT能否取到值。如果Debug窗口里中文变成问号说明这个封装版本的UTF-8转换有问题要么换封装版本要么在写入时手动用StrConv做一次转码。冒烟测试数据库smoke.db3测完直接删除它只是用来确认环境不参与正式业务。3. 在VB6里跑通数据操作litex_sqlite的建表、查询与可视化核对3.1 建表与字段类型SQLite的类型亲和性在VB6下的表现SQLite的字段类型和VB6并不一一对应它采用的是类型亲和性写入时按实际值推断存储方式。VB6里最常用的映射关系是Long对应INTEGER、Double对应REAL、String对应TEXT日期时间在SQLite里没有原生DateTime类型通常用TEXT存成“YYYY-MM-DD HH:MM:SS”格式排序和比较都方便。CREATE TABLE IF NOT EXISTS customer ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, created_at TEXT NOT NULL DEFAULT (datetime(now, localtime)) );这个建表语句里id用INTEGER PRIMARY KEY AUTOINCREMENT每插入一条自动加一对应VB6里的Longname用TEXT NOT NULL对应VB6的Stringcreated_at用TEXT默认值取数据库服务器本地时间VB6端读出来就是一个普通字符串想显示成用户习惯的格式就直接Format一下不需要额外转换。建表时有一个容易忽略的点如果老程序之前用Access字段名里可能带空格或者中文SQLite都支持但在SQL语句里必须用双引号或方括号括起来。更稳妥的做法是建表时全部用英文小写字段名程序里再通过别名映射成中文标题显示这样SQL语句干净也避免不同封装对大小写的处理不一致。3.2 插入与参数绑定避开SQL注入和中文乱码很多VB6老代码写SQL喜欢直接拼字符串比如cn.Execute INSERT INTO customer(name) VALUES( Text1.Text )这种写法在SQLite里有两个隐患一是文本里带单引号时直接截断SQL轻则报语法错重则被构造出恶意语句二是封装层在把VB6的Unicode字符串转成SQLite的UTF-8时拼接方式处理不好就容易变问号。用参数绑定可以一次性解决这两个问题。Dim cmd As LitexCommand Set cmd cn.CreateCommand(INSERT INTO customer(name) VALUES(?)) cmd.BindText 1, txtName.Text cmd.Execute Set cmd Nothing逻辑说明CreateCommand准备一条带占位符?的SQL语句BindText把第1个参数绑定成VB6的字符串Execute执行。整个过程没有出现单引号拼接文本里哪怕包含引号、换行、特殊字符都只被当作字段值处理。这种写法也符合SQLite官方推荐的方式SQL语句被预编译执行效率比临时拼接高。参数说明BindText是字符串绑定如果字段是整数用BindInt小数用BindDouble。占位符顺序从1开始SQL语句里有几个?就要按顺序绑几次。如果你的封装接口叫BindString或者SetParameterByName把名字换掉即可绑定思想一样。3.3 查询与结果集把SQLite数据灌进MSFlexGrid查询是VB6里用得最多的操作litex_sqlite把结果集封装成Recordset对象支持EOF、MoveNext这种和ADO很像的写法。下面这段从customer表取最近100条记录填充到MSFlexGrid里。Dim rs As LitexRecordset Set rs cn.Execute(SELECT id, name, created_at FROM customer ORDER BY id DESC LIMIT 100) MSFlexGrid1.Rows 1 MSFlexGrid1.TextMatrix(0, 0) 编号 MSFlexGrid1.TextMatrix(0, 1) 姓名 MSFlexGrid1.TextMatrix(0, 2) 创建时间 Do Until rs.EOF Dim rowIndex As Long rowIndex MSFlexGrid1.Rows MSFlexGrid1.Rows rowIndex 1 MSFlexGrid1.TextMatrix(rowIndex, 0) rs(id) MSFlexGrid1.TextMatrix(rowIndex, 1) rs(name) MSFlexGrid1.TextMatrix(rowIndex, 2) rs(created_at) rs.MoveNext Loop rs.Close Set rs Nothing逻辑说明Execute执行SELECT后返回结果集先移动指针到EOF之前再循环读取。rs(id)取字段值默认返回的是Variant赋给MSFlexGrid的TextMatrix时最好用 转成字符串避免NULL值显示成“空”或者触发类型错误。循环里先读取Rows得到当前行数再加一行写入数据这是MSFlexGrid最常用的追加方式。升级SQLite的update语句时也一样把字段名、表名替换掉即可但更新操作建议按下一章的事务和参数绑定方式做不要直接拼SQL。需要可视化核对数据时可以用DB Browser for SQLite打开db3文件界面里直接看表和WHERE条件过滤结果比自己写一行Debug.Print快得多。3.4 用DB Browser for SQLite核对数据程序和可视化工具对照验证程序写完界面后最怕的是“程序里看到的数据和数据库实际存的不一样”。这种情况多半不是SQLite问题而是封装层转换出错或者程序读的库文件与工具打开的不是同一个文件。我习惯在写完查询功能后先用DB Browser for SQLite打开db3文件右键表点“浏览数据”看name列的中文是否正常、id是否按序自增、created_at格式是否统一。DB Browser for SQLite也支持执行SQL和查看执行计划排查慢查询时非常有用。程序里报错说某个字段不存在但自己建表SQL里明明有——那就先用DB Browser的“数据库结构”面板确认表定义里到底有没有这个字段。如果工具里能看到、程序里读不到问题出在SELECT语句字段拼写或者结果集对象没有刷新如果工具里也看不到那就是表建错了程序一直在操作另一个库文件检查App.Path拼出的路径是否正确。4. 连接参数与并发写入让SQLite在老系统里更稳的几个设置4.1 用SetPragma控制并发与落盘busy_timeout、journal_mode、synchronous的合理取值SQLite的行为可以通过PRAGMA命令动态调整litex_sqlite封装一般提供SetPragma方法或者直接用Execute执行PRAGMA语句。对VB6老程序来说最值得改的是下面几个参数它们直接影响程序在多用户环境下的稳定程度。cn.SetPragma busy_timeout, 5000 cn.SetPragma journal_mode, WAL cn.SetPragma synchronous, NORMAL cn.SetPragma cache_size, 2000参数推荐值作用场景busy_timeout5000等待数据库锁释放的时间单位毫秒多客户端同时写时避免立刻报database is lockedjournal_modeWAL写入采用预写日志读和写可以并行读写并发高时比默认DELETE模式更稳synchronousNORMAL控制WAL模式下数据落盘的频率追求性能时用NORMAL怕数据丢失用FULLcache_size2000数据库页缓存页面数2000页约8MB频繁小查询时减少磁盘IO参数说明busy_timeout不是万能钥匙它只解决“竞争写锁”的等待问题如果某个连接一直持有写事务不释放等多久都没用。journal_mode设置成WAL后目录下会出现db3-wal和db3-shm两个附加文件这是正常现象不要随便删除否则可能导致数据回滚到上一次检查点。synchronous建议生成环境先用NORMAL观察如果业务方对“断电丢数据”容忍度低再改成FULL这个值影响的是写性能不是正确性。4.2 事务提交与回滚批量导入时不要一条一条自动提交VB6程序里最常见的性能杀手是循环里逐条ExecuteINSERT每执行一条SQLite就自动提交一次磁盘写操作要被反复刷新一万条数据可能要跑几十秒。正确做法是把一万条插入包在一个事务里所有写操作完成后一次性提交。Dim cn As LitexConnection Set cn New LitexConnection cn.Open App.Path \import.db3 cn.BeginTrans Dim i As Long For i 1 To 10000 Dim cmd As LitexCommand Set cmd cn.CreateCommand(INSERT INTO log(msg) VALUES(?)) cmd.BindText 1, row i cmd.Execute Set cmd Nothing Next i cn.CommitTrans cn.Close Set cn Nothing逻辑说明BeginTrans开启事务后所有INSERT都缓存在SQLite的日志里直到CommitTrans才真正落盘。中间任何一条失败执行RollbackTrans把之前插入的全部回滚不会留下半截数据。这在导入外部数据时特别重要比如从Access导出CSV再批量灌入SQLite事务包住后即便中途报错也能保证数据库还处于导入前的状态。注意事务的粒度一个事务不要开太久尤其不要在事务里夹MessageBox等用户交互否则事务长锁导致其他连接全部阻塞。批量导入建议每5000到10000条提交一次既保留回滚能力又能让其他进程有机会拿到写锁。4.3 单写者锁VB6程序碰上database is locked的常规处理SQLite的写锁机制是“单写者”同一时间只允许一个连接写入其他连接只能等。VB6程序如果开了多个窗口各自维护一个数据库连接用户同时点“保存”后点的那个就可能报database is locked。解决思路有两个方向一是全局只用一个写连接所有写操作穿行执行二是写操作带重试机制。Public Function SafeExecute(db As LitexConnection, sql As String) As Boolean Dim i As Integer For i 1 To 5 On Error Resume Next db.Execute sql If Err.Number 0 Then SafeExecute True Exit Function End If Err.Clear Sleep 300 * i Next i SafeExecute False End Function逻辑说明SafeExecute尝试执行SQL如果失败说明拿到锁等待时间按300、600、900毫秒递增最多等五次。Sleep是从kernel32导出的延时函数需要手工声明。这个方案把“一失败就报错”变成“先重试再报错”多用户点击保存的冲突大部分能被吸收。更彻底的做法是把程序里所有写操作收敛到同一个模块统一走一个公共函数由这个函数负责连接、重试和事务。这样虽然VB6是单线程但所有写操作在时间上错开锁冲突概率大幅下降。如果业务量真的高到单库撑不住那就不是改参数能解决的得考虑分库或换数据库引擎SQLite本身定位就是轻量嵌入式不适合高并发写入场景。4.4 SQLite数据库文件能否加密litex_sqlite普通版没有加密很多采购方会问“SQLite数据库文件能否加密”这是个敏感需求因为SQLite官方版本默认是明文存储用DB Browser for SQLite打开就能看到全部数据。litex_sqlite如果只是简单封装没有集成SQLCipher那它不具备透明加密能力。要加密只有两条路一是换用集成SQLCipher的SQLite版本封装层也必须对应支持二是业务字段单独加密比如身份证号、金额用AES加密后再存程序读取时解密。第一种方式改动大所有使用数据库的地方都要换DLL且旧库文件直接打不开需要导出导入。第二种方式改动小SQLite本身还是普通文件但敏感字段是密文即使文件被别人拷走也看不到明文。我一般建议先问清楚业务方的风险模型只是防运维人员误看那字段级加密够用如果是要防文件被拖走得用SQLCipher方案。5. litex_sqlite实战避坑5条让新手翻车的故障与修复5.1 找不到DLL入口点32位与64位DLL混用是最大翻车来源现象程序编译能通过启动后立即弹窗“无法定位程序输入点子sqlite3_open于动态链接库sqlite3.dll上”或者“找不到DLL入口点”。原因VB6程序是32位进程但机器是64位系统很多下载站默认给的sqlite3.dll是64位版本。64位DLL和32位程序根本不在一个进程空间里对不上入口点。另一个原因是litex_sqlite.dll和sqlite3.dll版本不配套封装层按某个版本编译队友换了更高版本的sqlite3.dll某些新增函数的入口找不到。解决确认exe编译选项里对齐方式是Win32再从可信来源下载32位版本的sqlite3.dll覆盖到exe目录同时保证litex_sqlite.dll和sqlite3.dll来自同一套发布包不要混搭。判断DLL是32位还是64位最简单办法是用VS自带的dumpbin命令或者依赖工具的属性页查看“计算机平台”字段。这类问题属于排查起来很玄学、原因其实很直观的典型。5.2 中文路径建了库却打不开Unicode文件名没被正确转换现象程序在App.Path为“C:\项目\数据\app.db3”时第一次能自动建库第二次运行就报“unable to open database file”但把exe和db3挪到纯英文路径就一切正常。原因SQLite的C接口接收的是UTF-8编码的文件名而VB6的字符串是Unicode。封装层如果偷懒只做了ASCII转换中文路径就会被转成乱码导致打不开或建到错误位置。解决先判断这个litex_sqlite版本是否支持Unicode路径查阅封装文档或者看接口说明不支持就换封装版本。如果封装版本暂时换不了程序层面用“数据目录固定在纯英文路径”的方式规避比如C:\Data或D:\AppData业务文件放在这个目录下界面显示中文不影响。路径里的中文问题最难排查因为它只在特定客户机器上出现本地英文环境完全正常。5.3 写入的中文读出来全是问号参数绑定时的编码没走UTF-8现象INSERT写入“张三”SELECT读出来变成“”用DB Browser for SQLite打开数据库看存储的已经是乱码不是显示层的问题。原因封装层的字符串转换只做了系统默认代码页的转换比如GBK而SQLite要求UTF-8。写入时乱码读出来自然也是乱码而且一旦乱码写入DB Browser打开就是损坏的中文靠查询时再转换已经救不回。解决插入时确认封装提供的是BindText而不是BindWString之类的区别。如果封装不支持正确的Unicode转换手工先转一下码再传入写之前把VB6字符串用StrConv转换为UTF-8字节数组。如果代码里用的是拼接SQL方式这类乱码会反复出现务必改成参数绑定。读取时也看一下rs(name)是否需要StrConv转换回来和写入是镜像关系常见封装如果写入正常读取也应正常两边都乱就要检查DLL版本是否配套。5.4 database is locked事务没提交的“黑匣子”锁现象程序运行一段时间后用户操作偶尔弹“database is locked”重开程序又好了频率不高但很烦人。原因写事务没有正常结束。常见场景是程序里执行了BeginTrans但没执行CommitTrans或者代码在事务中途因为异常直接跳到错误处理事务对象被释放但锁没释放。SQLite的锁生命周期很长一个进程崩溃前没揭秘这个库文件可能一直处于锁定状态其他连接只能等待。解决先处理规范问题BeginTrans和CommitTrans成对出现事务代码包在On Error里出错时统一走RollbackTrans再退出。再检查是否有多个连接同时写同一张表尽量收敛为单连接。如果库已经被锁住最简单办法是看有没有别的进程打开全部关闭后检查db3-wal文件是否存在WAL模式下残留的wal文件也可能被锁。数据库文件加密的问题如果叠加锁冲突排查难度会更大所以先把锁机制处理好再谈加密。5.5 删掉数据文件不减小SQLite不会自动释放已用页现象DELETE FROM customer删掉几千行数据库文件大小几乎没变化继续插入数据时文件继续增长。原因SQLite的DELETE逻辑是在页上打标记空间留给后续复用不会把文件裁小。这是设计行为不是bug也和有没有做VACUUM有关。文件增长到一定程度后即便表里数据很少备份和传输都会变慢。解决执行VACUUM命令重建数据库文件把空闲页回收掉。注意VACUUM需要额外磁盘空间因为它是把整库重新写入临时文件再替换磁盘满了会失败。周期性执行的频率看业务量一个月跑一次或者每次大范围清理后跑一次都行。删除大量数据之前先备份VACUUM是个重操作不要在业务高峰期执行。6. 用PRAGMA和VACUUM验证数据健康两个值得养成的检查习惯6.1 发布前跑一遍PRAGMA integrity_check每次发布新版本之前我习惯在测试环境对数据库文件跑一次完整性检查这能发现很多平时看不出来的页面损坏或索引异常。Dim rs As LitexRecordset Set rs cn.Execute(PRAGMA integrity_check) Debug.Print rs(0) rs.Close Set rs Nothing返回结果是“ok”表示完整其他任何提示都需要重视。常见异常值是“row X missing from index”或“page Y is never used”这些通常指向索引不一致可能是早期版本封装在写操作时崩溃留下痕迹。遇到这类提示优先从最新备份恢复不要尝试用SQLite修复损坏文件因为SQLite没有可靠的在线修复工具VACUUM也不能保证修复损坏。把一次完整性检查写进发布脚本能省掉后期大量现场排查时间。6.2 VACUUM与journal_mode检查除了完整性检查我还会顺带做两件事查当前journal_mode以及执行VACUUM整理文件空间。检查模式可以用一个小的List填充看结果是不是自己期望的WAL或DELETE。cn.Execute VACUUM执行VACUUM前确保数据库没有其他活跃事务也别在客户端运行期间做建议放在程序启动时检查一次并在后台低优先级执行或者作为运维脚本定期手动跑。WAL模式下VACUUM之后要检查db3-wal文件是否被清空如果wal文件仍然存在且很大说明还有连接没有关闭或没有到检查点强行拷贝db3文件会导致数据不完整。我现在每次交接老系统都给对方留一句话数据库目录下带wal/shm文件时别直接复制db3文件要先用DB Browser for SQLite关闭连接并检查一下或者干脆在程序退出后再拷贝。这些习惯看起来很基础但就是它们决定了一个SQLite方案能稳定跑五年还是三天两头出故障。希望帮到你。本文还有配套的精品资源点击获取