ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

VC++集成SQLite实战:从编译接入到API封装与性能优化指南

VC++集成SQLite实战:从编译接入到API封装与性能优化指南 简介面向在 Visual C 环境下集成 SQLite 的开发者这份资源以 SqliteTest 工程为基础演示了从环境配置、数据表操作到界面展示的完整流程。压缩包包含 61 个文件涵盖 sqlite3.h、sqlite3.dll、sqlite3.lib 等核心库文件以及 cpp、h、rc、res 等 Visual C 工程源码与界面资源另有多个 db 数据库样本和编译生成的 exe、pdb 等文件整体约 32MB便于直接打开工程查看运行效果。已有 199 人浏览学习。资源重点解决了大批数据快速插入与 ListCtrl 控件展示查询结果两大常见需求通过封装好的工程代码开发者可直接借鉴 sqlite3_open、sqlite3_exec、sqlite3_prepare_v2 等 API 的用法并掌握多语句一次性执行、游标遍历结果集等优化思路。工程内附带多个日期的 db 文件可作为测试数据检验插入与读取逻辑适合正在学习桌面数据库应用开发或需要在 VC 项目中低成本引入 SQLite 的初中级开发者。1. 写在前面为什么要在VC里用SQLite在VCVisual C工程里接入SQLite这事我前前后后做过好几轮了。第一次踩了一堆坑后来把流程固定下来基本就是下载源码、编译、封装API、处理业务场景这几步。如果你正好在Windows下用C写桌面程序又没有太大的数据库需求SQLite是特别顺手的一个选择。SQLite本身是一个嵌入式关系型数据库没有独立的服务进程所有数据都存放在一个单一的文件里。它天然适合桌面软件、工具类程序、嵌入式设备这类场景比如聊天记录存储、配置数据管理、本地缓存、单据流水记录等。相比直接用文件读写它有完整的SQL语法、事务机制和索引能力相比接入MySQL、SQL Server这类服务端数据库它又不需要安装、不需要配置账号密码、不需要考虑网络连接直接把库文件拷到工程里就能用。在VC环境中SQLite的接入方式非常灵活既可以编译成静态库也可以编译成DLL动态加载还可以直接把sqlite3.c源文件塞进工程一起编译这是它最大的优势。这篇文章面向的是在Windows平台上用Visual CVC6到VS2022都适用开发桌面程序的开发者无论你是刚接触SQLite的新手还是已经用过但想搞清楚进阶用法本文提到的几个核心环节——编译接入、API封装、业务场景落地、表结构升级、典型问题排查——基本能覆盖日常开发的绝大部分需求。2. 准备工作怎么把SQLite装进VC工程2.1 获取SQLite源码和DLLSQLite官方提供两种获取方式。第一种是直接下载预编译的DLL里面包含sqlite3.dll和sqlite3.def适合想要动态加载、减少编译时间、避免重复编译的场景。下载后使用lib /def:sqlite3.def /machine:x8632位或lib /def:sqlite3.def /machine:x6464位生成对应的.lib导入库然后在VC工程的链接器输入中添加这个.lib。第二种是下载合并后的源码文件sqlite-amalgamation这是我最推荐的方式。压缩包里只有sqlite3.h、sqlite3.c、sqlite3ext.h三个文件直接把sqlite3.c添加到工程里和你的C代码一起编译就行。这样做的好处非常明显调试的时候可以F11直接跟进SQLite源码内部查看SQL执行的具体行为发布的程序不需要依赖外部DLL单exe拷贝到任何Windows机器上都能跑。文件大小方面sqlite3.c完整版本大概9MB左右编译一次会多花十来秒但换来的是没有任何动态库依赖特别省心。2.2 配置VC工程编译选项如果你使用VC6通常是直接新建一个Win32 Console Application或MFC App然后把sqlite3.c拖进工程。这里有个经验一定不要用/ML或/MT等静态CRT链接方式与包含/MD的工程混用否则在运行时可能遇到内存管理崩溃因为SQLite内部申请内存和释放内存的C运行库不一致会导致指针无效。建议统一使用多线程调试DLL/MDd或多线程DLL/MD这是VC6以来的默认值一般不用改。还有一个坑SQLite源码是C文件VC默认按C语言编译没问题但如果你的工程设置了“编译为C”选项或者你把sqlite3.c改名为sqlite3.cpp编译器就会因为C和C的语法规则差异报错。正确的做法是保持.sqlite3.c后缀如果你非要统一文件名记得在文件属性里指定“不作为C编译”。在较新的Visual Studio版本中不需要额外设置预处理器宏sqlite3.c开头的配置已经满足需求。但如果你需要启用某些扩展功能比如FTS5全文搜索、JSON1扩展、RTREE索引就需要在工程预处理定义中添加SQLITE_ENABLE_FTS5 SQLITE_ENABLE_JSON1 SQLITE_ENABLE_RTREE SQLITE_ENABLE_COLUMN_METADATA这些宏需要在C/C → 预处理器 → 预处理器定义里添加不同VS版本路径稍有差异但原理相同。2.3 典型的动态加载方式上面说的直接编译源码是“静态接入”还有一种场景是数据库功能要作为插件或者单独模块动态加载。这时可以把SQLite编译成DLL然后在程序里用LoadLibrary加载#include windows.h #include sqlite3.h typedef int (*SQLITE3_OPEN)(const char*, sqlite3**); typedef int (*SQLITE3_CLOSE)(sqlite3*); typedef int (*SQLITE3_EXEC)(sqlite3*, const char*, int (*)(void*,int,char**,char**), void*, char**); HMODULE hDll LoadLibraryA(sqlite3.dll); SQLITE3_OPEN sqlite3_open_fn (SQLITE3_OPEN)GetProcAddress(hDll, sqlite3_open); SQLITE3_EXEC sqlite3_exec_fn (SQLITE3_EXEC)GetProcAddress(hDll, sqlite3_exec); // 使用时调用 sqlite3_open_fn(...)这种方式适合不想把SQLite源码混进主工程、且需要独立的数据库模块更新加载的场景。不过实际开发中我遇到的多数需求直接编译源码就够了所以本文后续内容均以直接编译源码的方式为前提展开。3. 核心API入门VC里操作SQLite的最小闭环3.1 打开和关闭数据库SQLite的操作逻辑非常简洁核心对象只有几个sqlite3*数据库句柄、sqlite3_stmt*预处理语句对象、sqlite3_open打开数据库、sqlite3_close关闭数据库以及sqlite3_exec直接执行SQL。打开数据库的代码#include sqlite3.h sqlite3* pDB NULL; int nRet sqlite3_open(test.db, pDB); if (nRet ! SQLITE_OK) { // 打开失败用 sqlite3_errmsg(pDB) 获取具体错误信息 const char* szErrMsg sqlite3_errmsg(pDB); printf(open db failed: %s\n, szErrMsg); return -1; }sqlite3_open如果传入的文件路径不存在会尝试创建一个新的数据库文件。这里有几个细节需要注意SQLite默认使用UTF-8编码而VC工程里中文路径通常是以GBK/ANSI编码传递的。如果你直接传E:\数据\test.db这样的ANSI字符串给sqlite3_open在简体中文Windows下路径里的中文字符会被解释成UTF-8导致无法打开或创建数据库。解决办法有两个一是使用sqlite3_open_v2结合sqlite3_initialize并自己进行编码转换二是把路径转换为UTF-8字符串后传入。我平时用的方案是封装一个Utf8转换函数将CString转换成UTF-8再调用打开接口。另外Windows下还有一个更直观的写法使用sqlite3_open16它接受UTF-16字符串可以直接把CStringW传进去CStringW strDbPath LE:\\数据\\test.db; sqlite3* pDB NULL; int nRet sqlite3_open16(strDbPath.GetBuffer(), pDB); strDbPath.ReleaseBuffer();使用open16就能绕开中文路径的编码坑。如果项目中统一使用ANSI字符串也可以先自行转换再打开就是多一层代码的事。关闭数据库时务必确保所有未完成的sqlite3_stmt对象都已经执行sqlite3_finalize释放掉否则sqlite3_close会返回SQLITE_BUSY数据库文件无法正常关闭后续文件操作甚至可能出现锁冲突。3.2 执行不返回结果的SQLsqlite3_exec创建表、插入、更新、删除等操作可以使用sqlite3_exec它一次性完成SQL的解析、执行和清理适合快速执行单条或多条语句。典型的建表示例const char* szCreateTableSQL CREATE TABLE IF NOT EXISTS t_user ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, age INTEGER DEFAULT 0, createtime DATETIME DEFAULT CURRENT_TIMESTAMP );; char* pErrMsg NULL; int nRet sqlite3_exec(pDB, szCreateTableSQL, NULL, NULL, pErrMsg); if (nRet ! SQLITE_OK) { printf(create table failed: %s\n, pErrMsg); sqlite3_free(pErrMsg); }注意sqlite3_exec的第三个参数是回调函数。如果执行的SQL返回了结果集比如SELECTSQLite会为每一行调用一次这个回调如果不需要处理结果传NULL即可。而第五个参数char** pErrMsg用于返回错误信息使用完毕后必须通过sqlite3_free释放否则会造成内存泄漏。IF NOT EXISTS是SQLite建表时非常实用的保护性写法可以避免重复建表导致报错。但要注意它只检查表名是否存在不会校验字段结构因此表结构变更时需要走我们后面说的升级流程。3.3 查询数据预处理语句和游标sqlite3_exec虽然可以直接执行SELECT但每次都要写回调函数传参传递也比较变扭。更推荐的查询方式是使用预处理语句prepared statement它讲SQL语句编译成字节码后可以重复执行也天然支持参数绑定能有效防止SQL注入。查询的完整套路如下sqlite3_stmt* pStmt NULL; const char* szSQL SELECT id, name, age FROM t_user WHERE age ?; int nRet sqlite3_prepare_v2(pDB, szSQL, -1, pStmt, NULL); if (nRet ! SQLITE_OK) { printf(prepare failed: %s\n, sqlite3_errmsg(pDB)); return; } // 绑定参数? 从1开始编号 sqlite3_bind_int(pStmt, 1, 18); // 逐行取数据 while (sqlite3_step(pStmt) SQLITE_ROW) { int nID sqlite3_column_int(pStmt, 0); const unsigned char* szName sqlite3_column_text(pStmt, 1); int nAge sqlite3_column_int(pStmt, 2); printf(id%d name%s age%d\n, nID, szName, nAge); } // 释放语句对象 sqlite3_finalize(pStmt);sqlite3_prepare_v2的第二个参数是SQL文本指针第三个参数是SQL长度传-1表示自动计算到字符串结束符。第四个参数是输出的预处理对象第五个参数指向SQL文本中未处理的剩余部分通常传NULL。使用sqlite3_step驱动查询时循环判断等于SQLITE_ROW则说明当前行有数据可以调用sqlite3_column_xxx系列函数按列索引取值列索引从0开始。取值时需要注意类型转换整数用sqlite3_column_int64位整数用sqlite3_column_int64文本用sqlite3_column_text数据量大的二进制用sqlite3_column_blob。3.4 错误处理和信息获取SQLite的大部分API都返回整数状态码判断这些返回值是保证程序健壮性的关键。以下几个状态码需要格外关注常量值含义SQLITE_OK0操作成功SQLITE_ERROR1SQL错误或发现数据库损坏SQLITE_BUSY5数据库文件被锁定多线程或进程并发访问时需要重试SQLITE_ROW100sqlite3_step返回的当前行有数据可读取SQLITE_DONE101sqlite3_step执行完成没有更多行数据SQLITE_CONSTRAINT19约束违反例如主键冲突、唯一约束冲突SQLITE_MISUSE21库被错误使用比如用未初始化的句柄实际开发时遇到SQLITE_BUSY的情况非常常见尤其是多线程同时写数据库时会频繁发生。我的处理方案是写一个带重试机制的exec封装出现BUSY就Sleep几十毫秒重试最多重试5次基本可以避免大部分锁冲突。4. 业务落地VC环境下SQLite的高频使用场景4.1 参数绑定防止SQL注入和数据转换问题很多人刚开始用SQLite时习惯用sprintf拼SQL字符串比如char szSQL[256]; sprintf(szSQL, INSERT INTO t_user(name, age) VALUES(%s, %d), szName, nAge); sqlite3_exec(pDB, szSQL, NULL, NULL, NULL);这样写的隐患很明显当szName中包含单引号、百分号或特殊字符时要么报SQL语法错误要么直接引发SQL注入。更麻烦的是如果名称里包含二进制数据或空字符这种拼接方式根本无法安全处理。正规做法是使用参数绑定。上面的插入语句改写为const char* szSQL INSERT INTO t_user(name, age) VALUES(?, ?); sqlite3_stmt* pStmt NULL; sqlite3_prepare_v2(pDB, szSQL, -1, pStmt, NULL); sqlite3_bind_text(pStmt, 1, szName, -1, SQLITE_TRANSIENT); sqlite3_bind_int(pStmt, 2, nAge); if (sqlite3_step(pStmt) SQLITE_DONE) { // 插入成功 } sqlite3_finalize(pStmt);参数绑定有几个核心函数需要记住sqlite3_bind_text绑定字符串第三个参数传字符串指针第四个参数传长度-1表示到结束符第五个参数传SQLITE_TRANSIENT表示SQLite会自行复制一份字符串数据传SQLITE_STATIC则不会复制要求字符串在语句执行期间保持有效。sqlite3_bind_int/sqlite3_bind_int64绑定整数。sqlite3_bind_double绑定浮点数。sqlite3_bind_blob绑定二进制数据。sqlite3_bind_null绑定NULL值。参数绑定不仅能防止注入还能让SQLite缓存SQL语句的编译结果重复执行时性能更好。这个方案在批量插入大量记录时优势非常明显相比每条SQL都要重新解析拼接性能差距可能达到数倍。4.2 高效批量插入事务与预处理语句的组合如果要一次性向数据库写入几千甚至几万条数据逐条调用INSERT会非常慢原因是每条INSERT都隐式开启了一个事务每次都要写日志文件并做磁盘同步。实际测试下来插入一万条记录逐条执行的耗时可能在几百毫秒到数秒之间而使用事务包裹后能缩短到几十毫秒性能提升十分显著。实现方式如下const char* szBeginSQL BEGIN TRANSACTION;; const char* szCommitSQL COMMIT;; const char* szInsertSQL INSERT INTO t_user(name, age) VALUES(?, ?); sqlite3_exec(pDB, szBeginSQL, NULL, NULL, NULL); sqlite3_stmt* pStmt NULL; sqlite3_prepare_v2(pDB, szInsertSQL, -1, pStmt, NULL); for (int i 0; i nCount; i) { sqlite3_bind_text(pStmt, 1, arrName[i], -1, SQLITE_TRANSIENT); sqlite3_bind_int(pStmt, 2, arrAge[i]); int nStepRet sqlite3_step(pStmt); if (nStepRet ! SQLITE_DONE) { // 插入失败处理 } sqlite3_reset(pStmt); // 重置语句允许重新绑定参数并再次执行 } sqlite3_finalize(pStmt); sqlite3_exec(pDB, szCommitSQL, NULL, NULL, NULL);这里有几个细节容易踩坑sqlite3_step执行完毕后sqlite3_reset必须在下次绑定前调用否则会返回SQLITE_MISUSE。如果某一条插入失败不能盲目继续执行最好判断一下原因如果是约束冲突导致失败但业务上可以忽略就继续如果是磁盘满或者数据库损坏应该回滚整个事务避免半途而废产生不一致的数据。事务的另一个关键点是事务一旦开始该连接就持有了写锁其他连接或线程在事务提交前无法写入。因此事务体内的操作要尽量精简不要穿插太耗时的外部调用比如网络请求或复杂计算否则容易导致其他模块等待锁超时。4.3 存在就更新不存在就插入UPSERT“存在就更新不存在就新增”是业务开发中非常高频的需求。在SQLite里如果你用的版本是3.24.0及以上官方提供ON CONFLICT DO UPDATE语法可以一条SQL完成这个逻辑不需要先SELECT判断再INSERT或UPDATE。SQLite版本3.24.0发布于2018年6月目前绝大多数环境中使用的最新版都远高于它可以直接使用。首先要保证表中存在唯一约束或主键约束例如CREATE TABLE IF NOT EXISTS t_score ( user_id INTEGER PRIMARY KEY, score INTEGER NOT NULL DEFAULT 0 );然后使用UPSERT语法const char* szUpsertSQL INSERT INTO t_score(user_id, score) VALUES(?, ?) ON CONFLICT(user_id) DO UPDATE SET score excluded.score;; sqlite3_stmt* pStmt NULL; sqlite3_prepare_v2(pDB, szUpsertSQL, -1, pStmt, NULL); sqlite3_bind_int(pStmt, 1, nUserID); sqlite3_bind_int(pStmt, 2, nScore); sqlite3_step(pStmt); sqlite3_finalize(pStmt);excluded关键字代表意图插入但发生冲突的那条数据。假如你插入user_id5, score90若表中已存在user_id5则UPDATE会将score更新为90。这条语法在并发情况下也比“先查再写”安全得多不需要额外锁定。如果你使用的SQLite版本较老不支持ON CONFLICT DO UPDATE那就只能靠事务加SELECT判断来实现SELECT UPDATE/INSERT的组合逻辑上等效但代码量会多一些性能也稍有下降。4.4 数据库升级给已有表增加表字段或新表桌面软件的数据库结构不是一成不变的。程序从V1.0升级到V2.0时往往需要给已有表增加新的字段、新建表、修改索引。SQLite没有ALTER COLUMN或DROP COLUMN的完整支持现代版本支持有限所以升级策略主要是ALTER TABLE ADD COLUMN加新列以及“建新表→导数据→换名”这类操作。推荐的做法是使用PRAGMA user_version记录数据库当前版本号。这个字段是SQLite内置的整数值专门用来做用户自定义的schema版本管理既不占用业务表也不会在导出时产生干扰。每次打开数据库时程序读取PRAGMA user_version低于当前程序期望的版本就逐级执行升级脚本。伪代码如下// 获取当前版本号 int nCurrentVersion 0; sqlite3_stmt* pStmt NULL; sqlite3_prepare_v2(pDB, PRAGMA user_version;, -1, pStmt, NULL); if (sqlite3_step(pStmt) SQLITE_ROW) { nCurrentVersion sqlite3_column_int(pStmt, 0); } sqlite3_finalize(pStmt); // 依次执行升级 if (nCurrentVersion 2) { sqlite3_exec(pDB, ALTER TABLE t_user ADD COLUMN email TEXT DEFAULT ;, NULL, NULL, NULL); sqlite3_exec(pDB, PRAGMA user_version 2;, NULL, NULL, NULL); } if (nCurrentVersion 3) { sqlite3_exec(pDB, CREATE TABLE IF NOT EXISTS t_log (...);, NULL, NULL, NULL); sqlite3_exec(pDB, PRAGMA user_version 3;, NULL, NULL, NULL); }升级每个版本时务必将PRAGMA user_version的更新放在同一个事务内。这样如果升级脚本中途失败整个事务回滚数据库版本号也不会被错误地提升下次启动时仍可重试升级。千万不要把版本号更新提前到脚本开头否则会出现版本号已经更新但表结构没有变更的情况后续程序运行会因为缺列而报错。ALTER TABLE ADD COLUMN时有一个限制新加的列如果带NOT NULL约束则必须提供非空的默认值否则旧记录无法满足约束。举例来说ALTER TABLE t_user ADD COLUMN addr TEXT NOT NULL;会报错因为表中已有记录的addr是NULL不满足NOT NULL要求。正确写法是ADD COLUMN addr TEXT NOT NULL DEFAULT ;。5. 多线程与工程实践中的避坑指南5.1 多线程访问SQLite的正确姿势很多桌面程序会开工作线程执行耗时查询同时UI线程也在操作数据库。SQLite默认的线程模式是serialized吗实际上取决于编译选项。官方文档说了三种模式single-thread、multi-thread、serialized。默认模式下SQLite是通过编译期宏SQLITE_THREADSAFE来控制默认值为1即serialized模式允许同一个连接被多个线程安全使用但每次只有一个线程能进入内部执行。如果使用默认配置多线程操作最稳妥的方法是每个线程创建自己独立的sqlite3*连接这样不共享句柄完全避免锁等待带来的意外行为。SQLite对同一数据库文件的多连接并发访问支持得很好读与读可以并行读与写、写与写则通过文件锁互斥。实际项目中我见过不少因为“多线程共享同一个sqlite3句柄”导致的卡死或崩溃排查起来非常头疼。如果你不能确保所有线程访问的时序就为每个线程单独开一个连接用完关闭。如果确实需要共享一个连接比如只在不频繁访问时使用可以在打开连接后执行sqlite3_busy_timeout(pDB, 3000); // 设置忙等待超时时间单位毫秒这样遇到锁冲突时SQLite会内部等待最多3秒而不是立即返回SQLITE_BUSY可以明显减少程序中需要人工重试的代码。5.2 内存泄漏与资源释放千万不能省finalizeSQLite的C接口是手动管理资源的。很多新手写代码时只关注打开、执行、关闭却忘了释放sqlite3_stmt语句对象或者忘了释放sqlite3_exec返回的错误信息内存。时间一长程序就会莫名其妙地占用内存越来越高。关闭数据库前必须确保所有语句对象已被sqlite3_finalize释放。为了降低出错概率我习惯把预处理语句的释放封装到RAII类中利用C的析构函数自动释放class SQLiteStmtGuard { public: SQLiteStmtGuard(sqlite3_stmt* pStmt) : m_pStmt(pStmt) {} ~SQLiteStmtGuard() { if (m_pStmt) { sqlite3_finalize(m_pStmt); m_pStmt NULL; } } private: sqlite3_stmt* m_pStmt; };使用时只需要sqlite3_stmt* pStmt NULL; sqlite3_prepare_v2(pDB, szSQL, -1, pStmt, NULL); SQLiteStmtGuard guard(pStmt); // 之后的代码如果异常退出也能确保释放5.3 中文字符串乱码的根源与解决在VC环境下SQLite最常遇到的乱码问题几乎都是编码不一致引起的。SQLite内部以UTF-8存储文本但VC的char*字符串在中文Windows下默认是GBK/ANSI编码。当你直接用GBK字符串调用sqlite3_bind_text时SQLite会把它当作UTF-8存储数据落到库里已经错了读取出来再按GBK显示自然就是乱码。解决思路有三种工程字符集设置为“使用Unicode字符集”代码里统一使用wchar_t宽字符调用sqlite3_open16、sqlite3_bind_text16、sqlite3_column_text16这一系列宽字符接口程序内部全程保持一致。所有写入SQLite的字符串先转成UTF-8读取出来后再转回GBK。这种方案兼容性好不依赖编译选项适合既有工程改造成本高的情况。你可以在代码里写一个CString和std::string之间的UTF-8转换函数。使用第三方库或C标准库的转换设施例如MultiByteToWideChar和WideCharToMultiByte组合但这类代码通常是重复劳动建议封装一次后全局复用。我个人的习惯是在较新的VS工程中直接用TEXT宏和宽字符接口减少转换代码在维护VC6老工程时则选择UTF-8转换方案。无论选哪种关键是要在项目初期就定好统一规则不要让团队中不同模块各搞一套否则数据库里的数据就会混入各种编码后续清理成本极高。5.4 一个典型的崩溃场景sqlite3误用示例之前排查过一个VC6程序的崩溃问题现象是运行一段时间后随机崩溃崩溃位置在sqlite3_step内部。最后定位到原因是某个模块在sqlite3_finalize之后又继续调用了sqlite3_column_text去读数据。这个场景非常典型使用已经释放的句柄访问内存在C里是未定义行为可能当时正常但一旦内存被其他对象复用就会崩溃。排查这种问题除了代码审查还可以开启SQLite的内存泄漏检测sqlite3_config(SQLITE_CONFIG_MEMSTATUS, 1);同时在退出时输出sqlite3_memory_used()的数值如果退出时内存占用不为0基本可以确定有资源没释放。当然最好的手段还是从编码习惯上规避所有SQLite对象严格遵循“配对使用”原则prepare对finalizeopen对closemalloc对free。6. 常见问题速查表平时积累的SQLite排查经验整理成一张表放这里方便你遇到问题时快速定位。现象常见原因解决方法打开数据库失败路径含中文打不开路径编码不是UTF-8使用sqlite3_open16或转换UTF-8插入的中文变成乱码绑定时传入GBK字符串统一转UTF-8后绑定或使用宽字符接口多线程写入时报database is locked写锁冲突未设置busy_timeout调用sqlite3_busy_timeout增加重试机制sqlite3_close返回SQLITE_BUSY存在未释放的stmt对象检查并finalize所有语句对象CREATE TABLE报already exists重复建表建表语句加IF NOT EXISTS升级时ALTER TABLE报duplicate column版本号管理错误导致重复执行采用PRAGMA user_version控制升级流程批量插入很慢未使用事务包裹使用BEGIN TRANSACTION和COMMIT包裹查询大数据量时内存持续增长每次取数据后未reset或finalize检查循环中的语句释放逻辑UPDATE语句无报错但数据没变WHERE条件不匹配常见于拼SQL时引号或空格错误打印SQL和实际参数检查sqlite3_step返回SQLITE_MISUSEstmt已被释放或未正确初始化确保prepare成功且未提前finalize排查SQLite问题时我首先会打开SQLite的sqlite3_trace_v2回调它会输出SQLite实际执行的每一条SQL语句和绑定参数非常适合定位“执行了但没效果”这类问题。在调试环境中可以在所有关键代码入口加上这个回调看SQL是否有拼写错误或参数值不符合预期。7. 聊聊我在实际项目中的体会SQLite在VC项目里属于“用起来很顺手坑全藏在细节里”的库。踩过几次编码和资源释放的坑之后我养成了一个习惯凡是涉及SQLite的代码一律不写裸指针裸调用都套上RAII封装或使用统一的工具函数哪怕多写几行模板也换来后续几个月的省心。另外一点感受比较深的是数据库升级流程。很多桌面软件上线后用户机器上存在各种历史版本的数据库文件升级逻辑必须非常保守一次只升一个版本每一步都要有日志和状态记录。用户数据库不是测试库一旦升级失败可能意味着用户的数据无法恢复所以升级前务必备份原文件可以简单地在升级前把test.db复制成test.db.bak升级成功后再删掉备份。最后分享一个小技巧SQLite提供一个特别实用的命令行工具sqlite3.exe调试时可以先用它打开数据库执行.schema查看表结构执行PRAGMA integrity_check;检查数据库完整性必要的时候用.dump导出全部数据。结合DB Browser for SQLite这款图形化工具基本能覆盖日常调试和数据检查的所有场景。开发环境中尽量多做这种前置校验比在代码里打日志猜问题高效得多。本文还有配套的精品资源点击获取
返回列表