SQLite 在 .NET 3.5 老环境下的 x64 二进制包部署指南
简介面向.NET Framework 3.5 SP1与Visual Studio 2008开发者的SQLite数据库引擎适配包专为64位系统设计。作为轻量级嵌入式数据库解决方案无需独立服务即可在桌面、服务端及富客户端应用中实现数据持久化与事务操作适合中小规模数据管理、本地缓存与离线业务。压缩包共21个文件总大小2.43MB以DLL程序集、XML文档、PDB调试符号为主另有可执行工具与配置文件。System.Data.SQLite相关程序集提供ADO.NET标准接口配套LINQ支持组件可让C#或VB.NET用类型化查询操作数据库XML用于代码提示PDB便于调试Northwind示例数据库帮助快速理解实体框架集成。已有249人学习下载。相比裸装SQLite原生库该整合包将托管提供程序、本机互操作、设计器与部署工具集中在一处减少64位环境下的配置工作量开发者可直接参考示例或将DLL嵌入项目快速获得可靠、免部署的数据库访问能力。1. 为什么还在给 .NET 3.5 配 sqlite一个 2008 年的 x64 二进制包解决什么sqlite-netFx35-binary-x64-2008-1.0.106.0 这个包名看一眼就知道是给老环境准备的。接手过工控上位机、还在维护 Windows Server 2008 R2 的人八成见过类似的目录生产机器不能随便装新运行时业务又需要本地 sqlite 数据库做离线缓存这时候最新版 System.Data.SQLite 的 NuGet 包装不上因为它的目标框架早就抬到 net4.x 甚至 netstandard 了。这个二进制包解决的就是这种被 .NET 版本卡死的场景在 .NET Framework 3.5 环境下通过 System.Data.SQLite.dll 调起 x64 版本的原生 SQLite 引擎正常建表、增删改查、开事务。适合两类人一类是被历史包袱锁在老框架上的维护工程师一类是刚接手老项目、需要在 VS2008 工程里加数据存储的新手。接下来的章节我会按拆包、部署、编码、踩坑、进阶的顺序把这个包从文件结构到运行机制完整过一遍。2. 从包名到部署文件目录、加载机制与选型逻辑2.1 包名五个字段拆解netFx35、binary、x64、2008、1.0.106.0官方发布包的命名不是乱写的每个字段都对应一个部署决策。先把整串拆开看字段含义部署影响sqliteSQLite 数据库引擎无netFx35编译目标为 .NET Framework 3.5只能在 3.5 及更高运行时上加载但不能倒过来拿到 .NET 2.0 用binary预编译二进制发布包拿到即用不需要自己编译源码x6464 位平台版本进程必须是 x64否则原生部分起不来2008对应 Visual Studio 2008 工具链原生 Interop DLL 由 VC9 编译目标机器可能需要对应运行库1.0.106.0System.Data.SQLite 程序集版本号决定 API 形态和 SQLite 原生引擎功能范围这里最容易被忽略的是 netFx35 和 2008 这两个字段。netFx35 意味着它绑定的是 .NET 3.5 的运行时你把它拖进一个 .NET 4.5 工程也能跑但没必要反过来如果你把新版 System.Data.SQLite 放到 .NET 3.5 工程里编译器直接报错因为新版程序集的元数据版本要求更高。我一般先用包名中的 netFx 段判断兼容性再决定要不要浪费时间解压。2.2 解压后的目录结构bin 目录与 x64 目录各管什么下载下来是一个 zip 压缩包里面最核心的结构是两层bin 目录放托管程序集x64 目录放原生程序集。拿到包以后我习惯先做一次干净解压观察目录层级再往工程里放。# 解压到专用目录避免和目标工程混在一起 $zip sqlite-netFx35-binary-x64-2008-1.0.106.0.zip Expand-Archive -Path $zip -DestinationPath .\sqlite-pkg -Force Get-ChildItem -Recurse .\sqlite-pkg | Select-Object FullName, Length这段命令用于把包解开并列出所有文件。注意老版本 Windows Server 上 PowerShell 可能没有 Expand-Archive 命令那个环境直接用 WinRAR 或 7-Zip 右键解压就行效果一样。解压后重点确认以下文件是否存在文件路径作用bin\System.Data.SQLite.dll托管包装程序集包含 SQLiteConnection、SQLiteCommand 等类x64\SQLite.Interop.dllx64 版原生混合程序集实际执行 SQL 的引擎bin\System.Data.SQLite.Linq.dllLINQ 支持程序集非必须按需引用doc\ 或 readme版本说明和变更记录有一个容易翻车的细节SQLite.Interop.dll 必须放在运行目录下的 x64 子文件夹里而不是和 System.Data.SQLite.dll 平级。这个加载规则是官方写死的谁改了谁踩坑。2.3 加载机制托管包如何找到原生引擎System.Data.SQLite.dll 本身是托管代码负责 ADO.NET 接口、连接串解析、参数绑定这些上层逻辑。真正的 SQLite 内核是一个 C 程序集被命名成 SQLite.Interop.dll。当你在代码里 new SQLiteConnection() 并调用 Open() 时托管程序集会在运行时根据当前进程位数去查找当前程序集目录下的 x64 或 x86 子目录把对应的 Interop 加载进来再执行 SQL。这就解释了两个现象。第一为什么只拷贝 System.Data.SQLite.dll 到目标机器一定会报错因为原生引擎根本没带上。第二为什么进程位数必须和包名里的 x64 一致因为托管程序集找 Interop 时是按架构目录找的找不到匹配项就只能抛 FileNotFoundException 或 BadImageFormatException。理解了这个机制后面部署就清楚该带哪些文件了。2.4 为什么不用 NuGet 新包版本选择的核心逻辑很多人会问直接 Install-Package System.Data.SQLite 不就行了问题在于 .NET 3.5 的老工程。现在的 System.Data.SQLite.Core 最低要求 .NET Framework 4.6.1装到老运行时的机器上连程序集都加载不起来。而 1.0.106.0 这个时代的 netFx35 版本API 形态和现在基本一致SQLiteConnection、SQLiteCommand、SQLiteDataReader 这些类名没变过等以后系统升级到 .NET Framework 4.8再把引用换掉、代码几乎不用改。所以选这个包不是因为它新而是因为它是在不升级运行时前提下最稳的选项。换个说法如果目标机器能装 .NET 4.8就没必要用 netFx35 包如果装不了那这个包几乎是唯一能正常跑通 C# SQLite 的方案。2.5 把包放入工程引用与输出目录配置在 Visual Studio 2008 里右键项目引用添加引用浏览到 bin\System.Data.SQLite.dll 即可。随后要做两件事第一项目属性里把“平台目标”从 AnyCPU 改成 x64第二在解决方案里新建一个名为 x64 的文件夹把 SQLite.Interop.dll 放进去属性里设置“复制到输出目录 – 如果较新则复制”。我一般会在工程里保留整个 x64 目录结构而不是手动去 bin 里粘贴。这种情况下每次重新编译生成时Interop 文件会跟着一起输出不会出现本地跑得好好的、发给别人就打不开的情况。3. C# 打开 sqlite 数据库最小可跑工程与连接串参数3.1 新建一个 .NET 3.5 控制台项目并完成引用用 VS2008 或 VS2010 新建控制台应用程序目标框架选 .NET Framework 3.5。如果你的开发机只有新版 Visual Studio可以在项目属性里直接切换目标框架为 3.5只要本机安装了对应的 Targeting Pack 就行。这一步走完代码里开始引入命名空间using System; using System.Data.SQLite; namespace SqliteNet35Demo { class Program { static void Main(string[] args) { Console.WriteLine(SQLite Demo Starting...); } } }这里引用的 System.Data.SQLite 命名空间来自第 2 章添加的托管 DLL不需要任何机器级注册。很多教程会让你改 machine.config 注册 DbProviderFactory那是旧版 ADO.NET 的老传统现在直接在代码里 new 连接对象就行没必要动全局配置省得出问题连累其他程序。3.2 连接串参数一句话控制并发与性能System.Data.SQLite 的连接串格式很接近 SQL Server核心差异在 Data Source 参数指向的是本地文件而不是服务器地址。连接成功后立刻建表并写入一条数据static void CreateAndInsert() { // Data Source 强烈建议写绝对路径相对路径受当前工作目录影响很大 string connStr Data SourceC:\data\demo.db;Version3;PoolingTrue;Cache Size2000;SynchronousOFF;; using (SQLiteConnection conn new SQLiteConnection(connStr)) { conn.Open(); string createSql CREATE TABLE IF NOT EXISTS user_info ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, age INTEGER); using (SQLiteCommand cmd new SQLiteCommand(createSql, conn)) { cmd.ExecuteNonQuery(); } string insertSql INSERT INTO user_info(name, age) VALUES(name, age); using (SQLiteCommand cmd new SQLiteCommand(insertSql, conn)) { cmd.Parameters.AddWithValue(name, 张工); cmd.Parameters.AddWithValue(age, 32); cmd.ExecuteNonQuery(); } string querySql SELECT id, name, age FROM user_info; using (SQLiteCommand cmd new SQLiteCommand(querySql, conn)) using (SQLiteDataReader reader cmd.ExecuteReader()) { while (reader.Read()) { string line string.Format({0} - {1} - {2}, reader[id], reader[name], reader[age]); Console.WriteLine(line); } } } }代码里的连接串参数逐项说明一下参数值说明Data SourceC:\data\demo.db数据库文件路径目录不存在会直接报错Version3SQLite 文件格式版本固定写 3PoolingTrue启用连接池避免频繁开关文件造成性能抖动Cache Size2000SQLite 内存缓存页数2000 页约等于 2MB 左右SynchronousOFF写入不再等待磁盘同步速度快但断电可能丢数据工控环境慎用有一个 .NET 3.5 专属的坑必须提醒这个版本的 C# 编译器不支持字符串内插也就是不能写 ${id} - {name}。我上面代码里用的是 string.Format这是老工程里最稳妥的写法。如果你习惯现代 C# 语法拿到这个工程后第一时间要改的往往就是这一处。3.3 参数化语句update、delete 与事务边界参数化不只是防止 SQL 注入在 SQLite 这种文件型数据库里参数化还能减少 SQL 解析开销语句复用效率更高。update 和 delete 的写法与 insert 一致都是用 前缀占位再通过 Parameters.AddWithValue 绑定值static void UpdateAndDelete(SQLiteConnection conn, int userId) { string updateSql UPDATE user_info SET age newAge WHERE id id; using (SQLiteCommand cmd new SQLiteCommand(updateSql, conn)) { cmd.Parameters.AddWithValue(newAge, 33); cmd.Parameters.AddWithValue(id, userId); int rows cmd.ExecuteNonQuery(); Console.WriteLine(string.Format(更新影响行数: {0}, rows)); } string deleteSql DELETE FROM user_info WHERE id id; using (SQLiteCommand cmd new SQLiteCommand(deleteSql, conn)) { cmd.Parameters.AddWithValue(id, userId); cmd.ExecuteNonQuery(); } }注意 AddWithValue 会自动推断参数类型但遇到日期时间类型时推断结果偶尔会偏稳妥一点可以在连接串里加 DateTimeFormatISO8601强制时间按文本格式存取。事务方面老工程最常见的错误是每次操作用一个独立连接SQLite 文件锁释放不及时多线程场景下很快就打出 database is locked解决办法是把多个写操作放进同一个事务using (SQLiteTransaction tx conn.BeginTransaction()) { try { // 在这里执行一批 insert / update / delete都复用同一个连接 tx.Commit(); } catch { tx.Rollback(); throw; } }事务缩短了文件锁的持有时间批量导入数据时效果非常明显。要是单条 SQL 里还嵌了循环可以把循环体外的连接和事务保持在最小范围内每 500 条提交一次这样既不会撑爆日志也不会长期锁文件。3.4 读取 BLOB 与日期字段老工程最容易出错的两个类型SQLite 没有独立的时间类型日期通常存成 ISO8601 文本字符串BLOB 则映射成 byte[]。读取时先把 reader 里的字段转成目标类型不要直接 One 字符串string selectSql SELECT avatar, created_at FROM user_info WHERE id id; using (SQLiteCommand cmd new SQLiteCommand(selectSql, conn)) { cmd.Parameters.AddWithValue(id, 1); using (SQLiteDataReader reader cmd.ExecuteReader()) { if (reader.Read()) { byte[] avatar reader[avatar] as byte[]; DateTime created DateTime.Parse(reader[created_at].ToString()); } } }写 BLOB 时用 AddWithValue 传入 byte[]插入后文件大小会自动增长。需要提醒的是字节数组较大时SQLite 建议分段写入或者用 SQLiteBlob 接口做流式读写一次性塞几百 MB 的 BLOB 会把整个文件锁死其他连接全部排队。4. x64 部署避坑Interop 加载失败、位数不匹配与运行库玄学4.1 现象运行时抛出 FileNotFoundException提示找不到 SQLite.Interop.dll第一次部署时很容易遇到这个本地开发机跑得好好的拷到服务器上双击就崩异常信息指向 SQLite.Interop.dll。原因很简单输出目录里没有 x64 子文件夹或者子文件夹里有文件但没被复制过去。SQLite.Interop.dll 不像 System.Data.SQLite.dll 那样会被 VS 自动随引用复制它默认不会出现在生成输出里。解决方式是把 Interop 文件按架构目录手动放进项目并确认其“复制到输出目录”属性为“如果较新则复制”。操作路径解决方案里新建 x64 文件夹把 SQLite.Interop.dll 拖进去右键属性里改 Copy to Output Directory。改完后重新编译去 bin\x64 目录确认文件存在再部署就不会报这个错了。4.2 现象BadImageFormatException程序集版本和位数不一致代码能编译运行时一执行就抛 BadImageFormatException网上搜一圈答案五花八门。出现这个异常时第一个要查的是项目平台目标。如果目标写成 AnyCPU在 64 位系统上进程默认是 x64这没问题但如果你调试环境是 32 位配置或者 IIS 里启用的是 32 位应用程序池那进程就是 x86 的加载这个 x64 版 Interop 必然爆异常。解决方式是统一位数项目属性 → 生成 → 平台目标选择 x64同时把配置管理器里的活动平台从 Any CPU 改成 x64。IIS 场景则在应用程序池高级设置里把“启用 32 位应用程序”设为 False。记住一个原则包名写 x64整个链路就必须 x64。4.3 现象混合模式程序集是针对运行时 v2.0.50727 生成的在 .NET 4.x 环境下加载这个 netFx35 包有时会看到类似“混合模式程序集是针对运行时 v2.0.50727 生成的在没有配置其他信息的情况下无法在 4.0 运行时中加载该程序集”的完整报错。原因是这个包的托管部分里包含了原生代码属于混合模式程序集CLR 4.0 默认不允许老混合程序集直接加载需要显式开启旧版运行时激活策略。解决方式是在 app.config 里加一段 startup 配置?xml version1.0 encodingutf-8 ? configuration startup useLegacyV2RuntimeActivationPolicytrue supportedRuntime versionv4.0 sku.NETFramework,Versionv4.0/ /startup /configuration这段配置的意思是允许在当前进程中加载使用旧 CLR 版本编译的混合模式程序集同时指定运行时版本是 v4.0。如果你目标环境是纯 .NET 3.5则把 version 和 sku 换成 v2.0.50727 即可。这个坑在迁移老工程到新机器时特别常见很多人误以为是 dll 损坏反复重发版本其实配置文件里一行就能解决。4.4 现象安装了所有依赖还是启动即崩进程无异常退出少数情况下程序直接退出事件查看器里能看到错误模块是 msvcr90.dll。这个包的 Interop 部分由 VS2008 编译依赖 VC9 运行库目标机器如果没装过 VC 2008 Redistributable x64进程在加载原生引擎时会因为缺少运行库而静默崩溃。这类问题比托管异常难排查因为托管侧看不到任何堆栈信息。解决方式是到微软下载安装对应运行库或在部署包里带上 msvcr90.dll 一起分发。检查方法很简单用 Dependency Walker 或直接看事件查看器里的错误模块名称。从那以后我每回部署 x64 的 System.Data.SQLite部署清单里都强制写上“VC 2008 x64 运行库”这一项宁可多装一个运行库也不要半夜被现场电话叫起来。4.5 现象database is locked多线程或 Web 场景读写挤死文件型数据库并发写是先天短板。高峰期多线程同时写SQLite 返回 database is locked。出现这个异常时先看在业务代码里是不是每次操作都 new 了一个新连接这样的话文件锁一直被不同短连接反复抢根本排不开。再看有没有事务长时间未提交事务持有 RESERVED 锁时其他写连接只能干等。常规解法是开连接池把连接复用起来并在连接串里加 Busy Timeout 参数Data SourceC:\data\demo.db;Version3;PoolingTrue;Busy Timeout3000;Busy Timeout 表示锁冲突时最多等待 3000 毫秒而不是立刻报错。如果并发量仍然高最后手段是改用 WAL 日志模式PRAGMA journal_modeWAL。WAL 模式允许读写并行读操作不会阻塞写操作这对多线程轮询场景是质变级的改善。缺点是 wal 文件会单独落盘备份时要连同 -wal 文件一起带走。5. 进阶用法连接池、WAL 与可视化验证5.1 用 Db Browser for SQLite 快速验收生成结果开发完以后我习惯用 Db Browser for SQLite 打开刚生成的 demo.db看一眼表结构和数据分布右键表名能看到建表语句是否和预期一致切换到“浏览数据”页签可以检查字符串编码、BLOB 大小和日期格式。这东西比任何调试输出都直观十几秒内能确认整条写入链路是否正常。只要 SQLite 的 .db 文件在自己手里验证成本非常低。5.2 数据维护VACUUM 与 integrity_checkSQLite 删除大量记录后文件不缩小这是它和传统数据库的明显差异。删除空间被标记为可复用物理文件体积不变。周期性执行 VACUUM 能整理碎片恢复空闲页PRAGMA integrity_check; VACUUM;第一条检查数据库完整性返回 ok 才安全执行 VACUUM。老工程里如果磁盘空间紧张建议在低峰期定时任务里跑一次。VACUUM 会重写整个数据库文件耗时和数据量成正比运行时不能有其他连接同时写。5.3 参数化配置把 SQLite 用成稳定的本地配置库相比 xml 配置文件SQLite 在频繁更新、多人操作、防并发写坏等方面都要强。常见做法是建一张 key_value 配置表把程序运行参数、点位表、校验规则都塞进去更新时用参数化语句覆盖单行天然具备事务回滚能力。推荐开启 WAL 模式 Busy Timeout并建议连接串固定写法Data SourceC:\data\app.db;Version3;PoolingTrue;Journal ModeWAL;Busy Timeout3000;SynchronousNORMAL;这套参数下读写并发表现稳定掉电时数据也不容易损坏足够覆盖大多数工控和个人项目的可靠性要求。结尾说一个小习惯现在每次部署带 SQLite 的程序我会强制走一遍自查清单——进程位数是不是 x64输出目录里有没有 x64\SQLite.Interop.dll目标机器装没装对应 VC 运行库。这三个点只要有一处不对现场九成翻车而且报错信息非常误导人。自己把这些检查固化成习惯之后这个 2008 年的老包反而变成了最省心的本地存储方案。希望这份拆解能帮你少走点弯路。本文还有配套的精品资源点击获取