简介:这份esqlite3 V1.1支持库与模块面向易语言开发中的SQLite数据库操作场景,在1.0基础上补齐互斥体、聚合上下文、自动提交、进度处理等全局命令,并为数据库与记录集增加事务锁状态、取行数、取数据库句柄等实用接口。资源共413个文件,压缩包17.39MB,核心文件类型涵盖易语言模块(e)、支持库文件(fne/dll)、C/C++源码(c/cpp/h)以及SQL脚本与工程配置(sln/vcxproj),可满足二次开发、学习封装或直接调用等多种需求。目前已有341人学习。值得关注的是,本版要求记录集手动关闭,避免内部自动关闭带来的资源隐患;数据库.开始事务()新增事务锁状态参数,对多线程场景尤为重要;附加数据库()支持密码参数,取记录集多个()可执行分号分割的多条SQL并获取全部记录集,这些改动使高并发读写和复杂查询的实现更稳妥、灵活。
1. sqlite3易语言支持库和模块 V1.1:多线程事务锁与加密,这次升级改了什么
做易语言数据库开发,绕不开 sqlite3 这个嵌入式库,而 esqlite3 这套支持库和模块最让人放心的地方,是它把 SQLite3 源码和 AES 加密 codec 都编译进了底层,不是简单的 ODBC 转发。V1.1 相对于 1.0 最值得关注的是多线程和事务控制:新增 S3 互斥体进入/退出、聚合上下文,给“开始事务()”加了事务锁状态参数,同时把记录集改成必须手动关闭。对需要做桌面收银、单机 ERP、多线程采集入库的易语言项目来说,这版直接影响数据库的并发稳定性和加密文件的可管理性。下面从文件结构开始,把这些更新拆到能直接复现的程度。
2. 从源码到支持库:sqlite3.c、codec.c 与易语言模块的分工
拿到资源包别急着在易语言里引用,先把文件过一遍。这里不是黑匣子,作者把 C 层源码一起放出来了,意味着你可以自己编译,也可以直接用预编译产物。
2.1 文件清单:哪些是 SQLite 本体,哪些是加密外壳
资源包里几个关键文件的角色如下:
| 文件名 | 作用 | 在 V1.1 里的定位 |
|---|---|---|
| configure.ac / Makefile.am | autotools 构建脚本 | 生成编译配置和 Makefile,决定怎么把 C 源码编成动态库 |
| appveyor.bat / appveyor-test.bat | Windows 自动编译脚本 | 在 AppVeyor 上自动编译并跑测试,保证源码包可复现 |
| sqlite3.c | SQLite3 官方合并源码 | 数据库核心引擎,所有 SQL 执行都走这里 |
| shell.c | SQLite 官方命令行 shell | 调试时直接命令行操作数据库 |
| rijndael.c | AES 算法实现 | 为加密扩展提供 Rijndael/AES 块加密原语 |
| codec.c | SQLite 页级加密扩展 | 在读写数据库页时加解密,这是“加密版 SQLite”的关键 |
可以看到,这个支持库不是纯易语言封装,而是先改了 SQLite C 层,再加上加密扩展,最后用易语言模块把命令暴露出来。所以你在使用时如果遇到“数据库文件被加密”“附加库打不开”这类问题,原因多半不在易语言代码,而在 codec 层。
2.2 编译与安装:从 autotools 到易语言支持库引用
如果你拿到的是源码包而不是成品 DLL,常见做法是用 MinGW 或 MSVC 先编译,再把编译出来的 sqlite3.dll 放到易语言程序目录。下面是一套最常见的编译流程:
./configure --enable-shared --prefix=/usr/local make make install gcc -o sqlite3_shell sqlite3.c shell.c codec.c rijndael.c -lpthread -ldl ./sqlite3_shell test.db "select sqlite_version();"这里的--enable-shared是让 autotools 生成动态库而不是静态库,prefix指定安装目录;“最后一行 gcc 编译 shell 测试程序”是为了验证 codec 和 rijndael 能不能正常链接。如果你的 configure 脚本里没定义SQLITE_HAS_CODEC,那加密命令会直接报“not compiled with encryption support”,这也是很多编译翻车的起点。
编译好的 DLL 放到 exe 同目录后,在易语言 IDE 里打开“工具 → 支持库配置”,勾选 esqlite3 支持库,或者在模块引用里添加对应的 .ec / .e 文件。验证是否安装成功,最直接的一段代码:
.版本 2 .支持库 esqlite3 .程序集 窗口程序集_启动窗口 .局部变量 数据库, zySqlite数据库 _启动窗口_创建完毕 数据库.打开 (“test.db”, , ) 数据库.执行 (“select sqlite_version()”) 输出调试文本 (数据库.取错误信息 ()) 数据库.关闭 ()这段代码的逻辑是:以默认方式打开当前目录下的 test.db,执行一条 SQL,然后输出支持库内部的错误信息。第一个参数是数据库文件名,第二个参数是打开参数,第三个参数是密码;V1.1 里密码通常和加密扩展配合使用,没加密的库直接留空。如果 DLL 没放对位置,打开()会返回假,取错误信息会提示“无法定位DLL”或“找不到指定模块”,先查文件路径。
2.3 命令分层:全局命令、数据库命令、记录集命令
V1.1 的命令不是平铺的,分成了三层:
- 全局命令:S3互斥体进入、S3互斥体退出、S3聚合上下文、S3取数据库自上下文。这些不属于某个对象,是支持库暴露的全局函数,用来在多线程里保护共享连接,以及在自定义函数回调里取上下文。
- zySqlite数据库命令:打开、关闭、执行、开始事务、附加数据库、取记录集多个、繁忙超时、繁忙处理、取文件名、是否只读、取互斥体、是否自动提交、进度处理、取下一记录集、取总影响行。
- zySqlite记录集命令:是否繁忙、是否只读、取数据库句柄、取行数、关闭等。V1.1 新增了前四项,目的就是让你能更细地控制记录集生命周期。
理解了分层,再去翻模块里的命令列表就不会乱。全局命令管并发,数据库命令管连接和事务,记录集命令管查询结果。下面一章逐个拆新增命令。
3. 新增命令逐个拆:互斥体、聚合上下文与数据库状态查询
这一章是对照 V1.1 更新说明逐条拆解。每条命令单独看都不难,难的是知道它应该在什么场景里用。
3.1 S3互斥体进入/退出:多线程写库的护身符
易语言做多线程时,很多开发者习惯用一个全局数据库变量到处调用。问题是,SQLite 默认的线程模式允许同一连接多个线程进入,但同一个事务的写操作被打断,容易出现 SQLITE_BUSY。V1.1 的互斥体命令本质是对支持库内部共享访问加临界区保护。典型用法是写操作前后各放一个:
.版本 2 .支持库 esqlite3 ' 多线程任务里写数据 子程序 线程_写入数据 (参数) S3互斥体进入 () ' 这里只做数据库操作,不要穿插耗时UI调用 数据库.开始事务 (1) ' 1 表示立即事务,见第4章 数据库.执行 (“insert into log(msg) values(‘hello’)”, , , ) 数据库.提交 () S3互斥体退出 () 子程序结束这里的逻辑是:进入互斥体之后,同一时刻只允许一个线程执行临界区里的代码,写事务不会被另一个线程的写操作打断。参数不用传,互斥体是支持库内部的全局资源。需要注意进入和退出必须成对,如果中间出现异常分支,很容易造成互斥体泄漏,其他线程全部卡死。所以在正式工程里,我一般会把进入和退出放到一个类的两个方法里,用对象生命周期保证必然成对。
V1.1 还加了“取互斥体()”命令,返回当前数据库连接关联的互斥体句柄。这个句柄可以传给其他线程,实现跨线程手动锁定,比全局命令更灵活。
3.2 S3聚合上下文与取数据库自上下文:自定义聚合函数的地基
SQLite 支持自定义聚合函数,比如实现一个自己的 group_concat。聚合函数需要 step 和 finalize 两个回调,每一步都要把中间结果存到上下文里。V1.1 的“S3聚合上下文”就是拿到这个中间结果指针,“S3取数据库自上下文”则是在回调里反查当前操作的数据库对象。
回调模型大概是这样:
.版本 2 .支持库 esqlite3 ' step 回调:每一行数据进入 子程序 聚合_累加 (上下文地址, 参数值) 局部变量 累计, 双精度小数型 累计 = 取聚合累计值 (S3聚合上下文 ()) 取聚合累计值_置 (累计 + 参数值) 子程序结束 ' finalize 回调:返回最终结果 子程序 聚合_结束 (上下文地址) S3取数据库自上下文 () ' 演示在回调里反查当前数据库 返回 (取聚合累计值 (S3聚合上下文 ())) 子程序结束这段代码里实际命令名以你拿到的模块导出为准,核心套路是:每次回调都用S3聚合上下文()来读写中间状态,而不是用易语言全局变量。原因很简单:多线程下全局变量会被互相覆盖,而聚合上下文是 SQLite 为每一条查询独立分配的,天然隔离。S3取数据库自上下文()的用途是,在聚合函数内部如果还要查其他表做关联统计,可以用它取得当前数据库对象,避免另行打开新连接。
3.3 zySqlite数据库新增命令:从繁忙超时到取下一记录集
V1.1 在数据库对象上新增了八条命令,我先给参数表再逐个说:
| 命令 | 参数 | 返回值 | 说明 |
|---|---|---|---|
| 繁忙超时 | 毫秒数 | 无或布尔 | 设置 busy_timeout,0 表示立即返回 |
| 繁忙处理 | 回调函数地址 | 无 | 自定义锁等待策略 |
| 取文件名 | 无 | 文本 | 当前数据库文件名 |
| 是否只读 | 无 | 逻辑值 | 当前连接是否只读 |
| 取互斥体 | 无 | 整数/句柄 | 当前连接关联的互斥体句柄 |
| 是否自动提交 | 无 | 逻辑值 | 假表示当前处于未提交事务中 |
| 进度处理 | 指令间隔, 回调地址 | 无 | 每执行 N 条虚拟机指令回调一次 |
| 取下一记录集 | 无 | 逻辑值 | 下一个结果集是否存在 |
| 取总影响行 | 无 | 整数 | 最近一次写操作影响的行数 |
“繁忙超时”和“繁忙处理”都是解决 SQLITE_BUSY 的,前者让支持库内部重试,后者让你自己决定等待策略。“进度处理”适合长查询,比如统计一个月订单量,每执行到一定数量的 SQLite 内部指令就回调一次,回调里可以更新进度条或标记取消。“取总影响行”和旧版“取受影响行”类似,但在多事务场景下更明确,它拿的是当前连接最近一次写操作的结果。
取多个记录集的命令组合起来是这样:
.版本 2 .支持库 esqlite3 ' 同时执行两条查询,分别取两个记录集 数据库.取记录集多个 (“select * from user; select * from log”, 记录集, ) 循环判断首 (记录集.是否结束 () = 假) 输出调试文本 (记录集.取字段值 (“name”)) 记录集.到下一条 () 循环结束 ' 第一个记录集处理完,取下一个 判断循环 (记录集.取下一记录集 ()) 输出调试文本 (记录集.取字段值 (“msg”)) 判断循环结束 ' V1.1 要求手动关闭 记录集.关闭 ()这段代码的逻辑是:一次性把两条 SQL 的结果都拉出来,先用循环处理第一个记录集,再通过取下一记录集()跳到第二个。参数上,取记录集多个()第一个参数是分号分隔的 SQL,第二个参数是输出记录集对象,第三个参数一般传空。这里最容易犯的错是把分号写在字符串字面量里,具体避坑见第 5 章。
4. 多线程事务与记录集生命周期:手动关闭和事务锁状态参数
V1.1 在事务和记录集上的改动,是这次升级里最影响现有代码行为的两个点。升级后如果还按 1.0 的写法跑,大概率会出现内存上涨或锁死。
4.1 开始事务新增事务锁状态参数:三个锁级别怎么选
SQLite 的BEGIN有三种锁级别,V1.1 把它们暴露成“开始事务()”的第二个参数:
| 参数值 | 事务类型 | 锁行为 | 适用场景 |
|---|---|---|---|
| 0 | DEFERRED | 读不加锁,第一次写才升级为写锁 | 单线程或只读 |
| 1 | IMMEDIATE | 开始事务后立即申请保留写锁,允许其他连接读 | 多线程写,推荐 |
| 2 | EXCLUSIVE | 立即申请排他锁,读写都不放行 | 数据库迁移、重建 |
为什么说这个参数在多线程里非常重要?因为默认的 DEFERRED 事务,两个线程可以同时读同一批数据,然后都想升级成写锁,互相等对方释放,最后死锁。IMMEDIATE 是写事务一开始就抢写锁,其他线程只能读,不能写,自然避免升级死锁。对应代码是:
.版本 2 .支持库 esqlite3 ' 多线程写操作,使用立即事务 数据库.开始事务 (1) 数据库.执行 (“update stock set qty = qty - 1 where id = 100”, , , ) 数据库.提交 ()参数 1 就是 IMMEDIATE。如果你用默认的 0,在并发不高时也能跑,但压测到多线程同时写就可能卡死。另外要注意,事务内如果执行出错,必须调“回滚()”,否则锁不会被 SQLite 释放,其他线程一直报“database is locked”。
4.2 记录集必须手动关闭:V1.1最关键的行为变更
V1.1 更新说明里明确写了“记录集必须手动关闭,任何内部方法都不再自动关闭”。这是最容易忽略的破坏性变更。
原因并不复杂:1.0 里“取记录集”用完后内部会顺手关闭,但 V1.1 为了支持“取记录集多个()”和多结果集跳转,记录集对象可能在取下一记录集之前还需要继续保留,内部不能再自动关闭。这就把释放责任交给了调用者。
正确的代码模式是这样:
数据库.取记录集 (“select * from user”, 记录集) 如果真 (记录集.是否结束 () = 假) ' 这里处理数据 记录集.关闭 ()这里的关闭()区别于“销毁”,“关闭”是释放底层 sqlite3_stmt 句柄,对象本身可以再次使用。我自己的习惯是查询完立刻关闭,绝不在下一次查询时才顺手关。旧版代码如果依赖自动关闭,升级后短时间内看不出问题,跑一个通宵任务就会暴露内存泄漏。
4.3 附加数据库增加密码参数:加密库怎么挂载
加密是这套支持库的一个卖点。SQLite 原版不带加密,esqlite3 通过 codec.c 在数据库页写入和读取时做 AES 加解密。V1.1 给“附加数据库()”增加了密码参数,让你可以挂载另一个带独立密码的加密库。示例:
.版本 2 .支持库 esqlite3 数据库.打开 (“main.db”, “main”, “123456”) 数据库.附加数据库 (“sub.db”, “sub”, “654321”) 数据库.执行 (“select * from sub.t_user”, , , )这里的逻辑是:主库用密码 123456 打开,附加库 sub.db 用密码 654321 挂载,挂载后通过别名 sub 访问它的表。第三个参数是新增的密码位,如果附加的库没加密,这个参数留空,传空字符串即可。如果密码不对,SQLite 会报“file is encrypted or is not a database”,这时候先检查是不是把主库密码误填到附加库上了。
5. 避坑手册:esqlite3 V1.1 多线程场景下的五个常见翻车点
V1.1 升级后我最常遇到的问题是下面这些,每条都是实际跑过的血泪经验。
5.1 记录集不关闭,内存和文件锁一起爆
**现象:**程序循环执行查询,内存持续上涨,用一段时间后数据库文件无法删除,甚至报“disk I/O error”。
**原因:**1.0 里内部自动关闭记录集,V1.1 改成手动关闭后,旧代码没有加“关闭()”,每次查询都泄漏一个 sqlite3_stmt。查询少时看不出来,循环几千次后就非常明显。
**解决:**把每一个取记录集操作都包上“关闭()”,最稳妥的办法是修改取记录集的地方,无论正常结束还是中途跳出,都在最后强制关闭记录集。升级完成后全项目搜索“取记录集”,逐一补关闭。
5.2 事务锁状态传错,多线程死锁
**现象:**两个线程同时写,偶尔卡在“database is locked”,重启程序又恢复,过一段时间再次出现。
**原因:**两个线程都用了默认 DEFERRED 事务,开始事务时不加锁,执行到写语句时才申请升级;互相持有共享锁等待对方释放,形成死锁。
**解决:**所有写线程的“开始事务()”都传参数 1,用 IMMEDIATE 事务提前占住写锁。再配合 S3互斥体进入/退出,把临界区范围控制到最小,基本能根除这个问题。
5.3 附加带密码数据库,参数传错导致“file is encrypted”
现象:“附加数据库()”调用成功,但查询附加库的表时报错“file is encrypted or is not a database”。
**原因:**附加库是加密库,但调用时没传密码;或者把主库密码误填到附加库密码位置。codec 层解密失败后才返回这个错误。
**解决:**确认附加库当初是用哪个密码初始化的,调用时严格按“文件名、别名、密码”顺序传参。主库和附加库密码可以不同,但要分开传。如果附加的是普通库,密码位留空,不要传一个非空字符串。
5.4 取记录集多个() 被分号打断,结果集数量对不上
**现象:**用分号拼接多条 SQL 调用“取记录集多个()”,返回的记录集数量比 SQL 条数少,或者取字段时提示“no such column”。
**原因:**SQL 字符串里如果出现分号,比如select 'a;b'这种字符串字面量,封装层如果简单按分号拆分,就会把 SQL 切碎,导致语句不完整。
**解决:**保持 SQL 语句里没有多余分号,尤其是字符串字面量。如果确实要传特殊字符,用参数绑定代替拼接。复杂场景建议分批次执行,不要迷信一次调用跑完所有语句。
5.5 繁忙超时和繁忙处理同时设,后一个覆盖前一个
**现象:**先调用“繁忙超时(3000)”,再调用“繁忙处理(回调地址)”,但等待超时后仍然立即报错,或者自定义回调根本没被触发。
**原因:**SQLite 底层 busy_timeout 和 busy_handler 是同一个机制的两面——设置后者会替换前者。封装层没有做共存处理,后设置的就覆盖了先设置的。
**解决:**二选一。简单场景用“繁忙超时(毫秒)”,设置后内部自动重试;复杂场景用“繁忙处理”,在回调里自己累计等待时间,决定是继续还是放弃。不要两个都设。
6. 进阶封装:用 V1.1 组合一个多线程安全的数据访问类
到这里,命令和坑都清楚了,剩下的是怎么把这些零散命令变成一个不容易用错的结构。
6.1 封装思路:互斥体 + 立即事务 + 强制关闭
我习惯把数据库操作封装成类,对外只暴露“初始化、写数据、查询数据、释放记录集”四个方法。内部用互斥体保护写操作,写事务全部用 IMMEDIATE,记录集在回调结束后立刻关闭。业务代码不需要关心锁细节。
.程序集 类_安全数据库 .局部变量 数据库, zySqlite数据库 子程序 写数据 (SQL语句) S3互斥体进入 () 数据库.开始事务 (1) 数据库.执行 (SQL语句, , , ) 数据库.提交 () S3互斥体退出 () 子程序 查询数据 (SQL语句, 记录集) 数据库.取记录集 (SQL语句, 记录集) ' 调用方处理完后必须调用释放记录集 子程序 释放记录集 (记录集) 记录集.关闭 ()这相当于把第 4 章的要点固化到代码结构里:互斥体保证同一时间只有一个线程在写,立即事务避免升级死锁,强制关闭避免泄漏。
6.2 压测验证:多线程写库不报错、内存平稳
封装完要压测,不然心里没底。我一般开四个线程,每个线程循环写 200 条记录,然后观察两点:是否出现“database is locked”,进程内存是否持续增长。测试片段:
计次循环首 (200, i) 类数据库.写数据 (“insert into load_test(id, tag) values(” + 到文本 (i) + “, ‘sync’)”) 计次循环尾 ()跑完后检查表里记录数是否等于理论值。如果出现锁错误,优先检查“繁忙超时(3000)”有没有在最开始设置;如果内存暴涨,优先检查记录集关闭。从那以后,我每次升级支持库都会强制走一遍这个流程:先改记录集关闭,再确认事务锁状态,最后压测半天。这套组合拳打下来,易语言里用 SQLite 的玄学问题基本绝迹。希望帮到你。
本文还有配套的精品资源,点击获取