简介:这是一份为 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,最不建议的是ADO+ODBC驱动。
| 路线 | 典型写法 | 优点 | 主要问题 |
|---|---|---|---|
| ADO + ODBC驱动 | 配置系统DSN后,cn.Open "Provider=MSDASQL;..." | 代码改动小,ADO写法熟悉 | 官方维护的ODBC驱动难找,部署时每台客户端都要配数据源 |
| 裸调sqlite3.dll | Declare Function sqlite3_open Lib "sqlite3.dll" ... | 不依赖第三方,DLL直接拷 | 要自己处理sqlite3_stmt指针、sqlite3_finalize,中文编码坑多 |
| litex_sqlite封装 | Set cn = New LitexConnection | 类名接近ADO,代码直白,封装了UTF-8和事务 | 需要在项目里引用DLL,资料相对少 |
ADO+ODBC我最早试过,发现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官方编译的DLL,litex_sqlite.dll是封装层,两者缺一不可。封装层如果是ActiveX DLL,首次部署时可能需要RegSvr32注册;如果封装被设计成免注册的Direct COM调用方式,那就不需要注册,直接用CreateObject或New引用即可。老机器如果连VB6程序都跑不起来,还要先补VB6运行库,最简单的判断方法是看msvbvm60.dll在不在System32目录里,不在的话用安装包补上。
| 文件 | 作用 | 备注 |
|---|---|---|
| sqlite3.dll | SQLite核心引擎 | 必须是32位版本,VB6程序是32位进程 |
| litex_sqlite.dll | 把C接口封装成VB6对象 | 与sqlite3.dll版本配套,不配套会报入口点错误 |
| msvbvm60.dll | VB6运行时 | 缺了它程序根本起不来 |
| 目标.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都置为Nothing,VB6里手动释放对象是好习惯,尤其是在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里的Long;name用TEXT NOT NULL,对应VB6的String;created_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_timeout | 5000 | 等待数据库锁释放的时间,单位毫秒 | 多客户端同时写时避免立刻报database is locked |
| journal_mode | WAL | 写入采用预写日志,读和写可以并行 | 读写并发高时比默认DELETE模式更稳 |
| synchronous | NORMAL | 控制WAL模式下数据落盘的频率 | 追求性能时用NORMAL,怕数据丢失用FULL |
| cache_size | 2000 | 数据库页缓存页面数,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方案能稳定跑五年还是三天两头出故障。希望帮到你。
本文还有配套的精品资源,点击获取