news 2026/10/13 1:24:56

.NET 3.5 老项目集成 SQLite:二进制包选型、部署与故障排查

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
.NET 3.5 老项目集成 SQLite:二进制包选型、部署与故障排查

简介:面向64位Windows平台且使用.NET Framework 3.5 SP1的开发者,这份压缩包提供SQLite 1.0.106.0的完整互操作组件,专为Visual Studio 2008项目设计。包体共21个文件,涵盖System.Data.SQLite.dll主提供程序、SQLite.Interop.dll原生互操作层、System.Data.SQLite.Linq.dll等4个核心DLL,辅以PDB调试符号、XML注释文档、示例数据库northwindEF.db与安装程序Installer.exe,压缩后仅2.43MB,方便随项目分发。已有251人学习下载。开发者可获得开箱即用的ADO.NET数据访问能力:通过System.Data.SQLite.dll执行SQL事务和丰富查询,借助System.Data.SQLite.Linq.dll在C#或VB.NET中直接使用LINQ语法操作SQLite;内置的Northwind示例库可用于快速验证Entity Framework集成,Installer.exe则简化目标机器部署。所有配置文件帮助预设连接字符串与运行参数,PDB与XML配套便于调试和智能提示,是经典.NET 3.5环境下嵌入式数据库开发的实用工具包。

1. 这个二进制包到底在解决什么问题

如果你还在维护一套 .NET Framework 3.5 时代的老系统,用的是 Visual Studio 2008 编译,现在想加一个本地数据库,又不愿意为此部署 SQL Server 或 MySQL,那么 sqlite-netFx35-binary-x64-2008-1.0.106.0 就是绕不开的那个选择。它是 System.Data.SQLite 的预编译二进制发行包,目标框架是 netFx35,运行在 64 位进程里,不需要额外装任何数据库服务,引用两个 DLL 就能开工。

这个标题看着长,每个字段却都是硬约束:netFx35 对应 .NET 3.5 运行时,x64 指定进程位数,2008 表示面向 VS2008 项目,1.0.106.0 是这个驱动发布版本。很多老工程师拿到包就直接复制到 bin 目录,结果分别栽在位数不对、Interop.dll 没放对子目录、连接串路径飘了这几个问题上。

这篇笔记按我维护老项目时的实际顺序来写:先讲选型和文件摆位,再给能直接抄的 CRUD 与事务代码,然后列出这个版本高频出现的五个坑,最后留一个自检函数。新手可以照步骤做,熟手也能看看参数边界在哪里。

2. 选型与落位:为什么是 netFx35 + x64,文件又该怎么摆

2.1 升级框架还是锁定 netFx35,先算清楚这笔账

老项目升级框架是牵一发动全身的事。一个 VS2008 解决方案里可能混着十几个工程,某个第三方控件只支持 .NET 3.5,升到 4.x 之后先冒出来一堆警告,紧接着是运行时行为差异,最后还是要回滚。我之前接过一个维护项目,仅是把一个公共程序集从 3.5 升到 4.6,就带出了一个反射加载顺序的兼容问题,测试组排了一周。从那以后,我给自己定了个规矩:老系统能不能跑,优先保持原运行时,需要新功能就在这个版本内找方案。

SQLite 驱动的发布包同样遵循这个逻辑。System.Data.SQLite 的二进制包按 netFx20、netFx35、netFx40、netFx45 等前缀区分托管层编译目标,标题里的 netFx35 就是专门给 .NET 3.5 准备的。版本 1.0.106.0 是这个驱动的发布号,同一发布号下会同时提供多套二进制,选错套件不会立刻报错,但会在某些 API 上出现“找不到方法”或类型加载失败。与其赌兼容性,不如一开始就锁定 netFx35。

x64 字段则对应部署目标。生产服务器是 64 位系统,应用程序池或 Windows 服务进程跑在 64 位,那么驱动里的 SQLite.Interop.dll 必须是 64 位版本。如果你拿 x86 包硬跑在 64 位进程里,结果是 BadImageFormatException。这个不是 SQLite 的毛病,是 Windows 加载 PE 文件时对位数不一致的强约束。

2.2 文件落位:System.Data.SQLite.dll 与 SQLite.Interop.dll 各放哪里

解压标题对应的包之后,你默认应该得到两个关键文件:System.Data.SQLite.dll 是托管主程序集,负责实现 ADO.NET 接口;SQLite.Interop.dll 是原生互操作库,负责真正读写 .db 文件。运行时托管程序集根据当前进程位数,去约定的子目录里找后者。

一个常见做法是直接把所有 DLL 平铺进 bin 根目录,这样会在运行时抛“未能加载文件或程序集 SQLite.Interop.dll”。原因不是文件不存在,而是路径没按照约定放。下面这个目录结构是关键:

# 解压后的典型目录结构,x64/x86 子目录最好保持原样 /your-app/bin/ |-- System.Data.SQLite.dll # 托管主程序集 |-- System.Data.SQLite.Linq.dll # 可选的 LINQ 支持,不用就不放 |-- x64/ | `-- SQLite.Interop.dll # 64 位原生库,文件名不要改 |-- x86/ | `-- SQLite.Interop.dll # 32 位原生库,部署时按需保留

这里面的 x64/x86 子目录名是驱动内部的查找约定,运行时会在当前应用基目录下按进程位数这个子目录去找 SQLite.Interop.dll。所以复制文件时不要再重新规划目录结构,最好是整个解压目录按原样并入发布包。只要托管程序集和原生库位数一致,文件路径正确,加载就不会翻车。

2.3 在 VS2008 项目里把平台目标固定成 x64

项目属性里的“平台目标”在 VS2008 中默认是 AnyCPU。这里的坑在于 AnyCPU 在 64 位操作系统上会运行 64 位进程,如果你手误引用了 x86 包,运行时加载原生库就会格式不匹配。为了从源头统一,我习惯直接修改 .csproj,把平台目标写死成 x64:

<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|x64' "> <PlatformTarget>x64</PlatformTarget> <OutputPath>bin\</OutputPath> <DefineConstants>TRACE</DefineConstants> <Optimize>true</Optimize> </PropertyGroup>

这段配置告诉 MSBuild:Release 配置且平台为 x64 时,生成的目标平台是 x64,输出目录直接使用 bin。如果你还保留 Debug 配置,同样要写明<PlatformTarget>x64</PlatformTarget>,否则开发机上一调试,进程又变回 AnyCPU。

有一点要单独提醒:标题里的 2008 表示这个包面向 VS2008,但并不是说你只能在 VS2008 里用。只要项目的目标框架是 .NET 3.5,后面的 VS 版本也能引用它。真正约束的是程序集的目标框架,不是 IDE 版本。

2.4 在 App.config 里注册 providerFactories

如果是自己直接 new SQLiteConnection,这节可以跳过。但如果老项目的数据访问层用了 DbProviderFactories,或者想通过 DbProviderFactory 创建连接以便以后替换数据库,就需要在 App.config 里注册。配置长这样:

<system.data> <DbProviderFactories> <add name="System.Data.SQLite" invariant="System.Data.SQLite" description=".NET Framework Data Provider for SQLite" type="System.Data.SQLite.SQLiteFactory, System.Data.SQLite, Version=1.0.106.0, Culture=neutral, PublicKeyToken=xxx" /> </DbProviderFactories> </system.data>

这里的 invariant 字符串System.Data.SQLite是外部代码查找 provider 时用的固定标识,不要随意改。type 字段里的 Version 要与实际引用的程序集一致,PublicKeyToken 必须是真实值,不能用 xxx 占位。

很多人在这里踩坑:手抄网上的配置,PublicKeyToken 是另一版本的值,结果程序集加载时匹配不上。我一般写一个小工具从引用 DLL 里读公钥标记,后面避坑章节会给出读取代码。注册完成之后,用DbProviderFactories.GetFactory("System.Data.SQLite")就能拿到工厂实例。

2.5 用最短的代码验证程序集可加载

配置做完先别写业务,先跑一段最小验证。用内存库作为数据源,不需要磁盘文件,能最快暴露位数和路径问题:

using System; using System.Data.SQLite; class VerifySqlite { static void Main() { using (SQLiteConnection conn = new SQLiteConnection("Data Source=:memory:")) { conn.Open(); Console.WriteLine("Provider version: " + typeof(SQLiteConnection).Assembly.GetName().Version); Console.WriteLine("Engine version: " + conn.GetSQLiteVersion()); Console.WriteLine("Is 64-bit process: " + (IntPtr.Size == 8)); } } }

Data Source=:memory:会创建一个纯内存数据库,既不会污染磁盘,也不需要确认路径权限。GetSQLiteVersion()返回底层 SQLite 引擎的版本号,用它确认驱动确实加载成功。IntPtr.Size == 8表示当前进程是 64 位。如果运行到conn.Open()就抛异常,优先查进程位数和 SQLite.Interop.dll 的路径。

3. 核心用法:连接字符串、参数化 CRUD 和批量事务

3.1 连接字符串:路径、版本和超时

System.Data.SQLite 的连接字符串与其他 ADO.NET 驱动类似,但有几个参数含义必须分清。Data Source指定数据库文件路径,可以是相对路径也可以是绝对路径;Version=3表示使用 SQLite 3.x 文件格式,不是驱动版本;Pooling决定是否启用连接池。下面是常用配置:

string connStr = "Data Source=D:\\data\\saas\\app.db;" + "Version=3;" + "Pooling=True;" + "Max Pool Size=50;" + "Default Timeout=30";

Max Pool Size在多线程同时访问时会起作用,默认值是 100,对于一个小型业务系统 50 已经足够。Default Timeout是命令执行等待秒数,当另一个连接持有写锁时,等待时间到了就报“database is locked”。这里尤其要注意文件路径的处理。

在 IIS 或 Windows 服务环境下,当前工作目录经常指向System32,相对路径的数据库文件会建到莫名其妙的位置。我会避免用相对路径,优先用绝对路径,或者基于应用程序基目录拼接。路径变量单独放配置里,方便每台服务器调整。

3.2 参数化 CRUD:不手工拼接字符串

老工程师里有种坏习惯是喜欢用字符串拼接 SQL,这在 SQLite 上同样危险。参数化不仅防止注入,还能让 SQLite 的语句缓存生效。下面是一个插入方法:

public void SaveOrder(SQLiteConnection conn, string orderId, decimal amount) { const string sql = @" INSERT INTO orders(order_id, amount, created_at) VALUES(@orderId, @amount, datetime('now','localtime'))"; using (SQLiteCommand cmd = conn.CreateCommand()) { cmd.CommandText = sql; cmd.Parameters.AddWithValue("@orderId", orderId); cmd.Parameters.AddWithValue("@amount", amount); cmd.ExecuteNonQuery(); } }

参数名前缀用 @ 是 SQLite 的常见写法,参数名必须和 SQL 文本里的占位符严格一致。AddWithValue会自动推断类型,但在批量写入且数据类型明确时,我一般会改成显式创建 SQLiteParameter,把 DbType 指清楚,减少类型转换损耗。这里datetime('now','localtime')是 SQLite 内置函数,写入的是本地时间,避免在代码里再传一次时间参数。

查询同样用参数化:

public Order GetOrder(SQLiteConnection conn, string orderId) { const string sql = "SELECT order_id, amount FROM orders WHERE order_id = @orderId"; using (SQLiteCommand cmd = conn.CreateCommand()) { cmd.CommandText = sql; cmd.Parameters.AddWithValue("@orderId", orderId); using (SQLiteDataReader reader = cmd.ExecuteReader()) { if (!reader.Read()) return null; return new Order( reader.GetString(0), reader.GetDecimal(1)); } } }

SQLiteDataReader 必须用完即关。不要在外面留着 reader 不释放,否则连接池里的连接会被一直占用,后续命令会排队等锁,表现成“第一次调正常,第二次开始卡住”。

3.3 批量导入:一条条 insert 会悔到肠子青

如果业务是一次性导入几千或几万条记录,不要逐条执行 insert。每执行一次命令,SQLite 都默认自动提交一次事务,也就是把数据刷到磁盘一次,几千条就是几千次磁盘 IO,慢得让人以为是程序假死。用事务包住以后,数据先留在日志和内存里,最后一次性提交,速度能差一个量级以上。

public void ImportBatch(List<Order> orders) { string connStr = "Data Source=D:\\data\\saas\\app.db;Version=3;"; using (SQLiteConnection conn = new SQLiteConnection(connStr)) { conn.Open(); using (SQLiteTransaction tx = conn.BeginTransaction()) { const string sql = @" INSERT INTO orders(order_id, amount, created_at) VALUES(@orderId, @amount, datetime('now','localtime'))"; using (SQLiteCommand cmd = conn.CreateCommand()) { cmd.CommandText = sql; cmd.Transaction = tx; // 显式绑定到当前事务 foreach (Order item in orders) { cmd.Parameters.Clear(); cmd.Parameters.AddWithValue("@orderId", item.OrderId); cmd.Parameters.AddWithValue("@amount", item.Amount); cmd.ExecuteNonQuery(); } } tx.Commit(); } } }

cmd.Transaction = tx这行必须写,不然命令默认在事务外执行,你开事务等于白开。Parameters.Clear()也是必要的,否则上一次循环的参数对象会一直被保留,内存和解析成本都会缓慢增长。Commit 之前如果发生异常,直接让 using 块触发回滚即可。批量导入后记得再执行一次PRAGMA wal_checkpoint或让事务自然落盘。

4. 实战避坑:这个版本绕不开的五个高频问题

4.1 BadImageFormatException:32 位与 64 位打架

现象:编译一切正常,运行到创建连接或打开数据库时抛出BadImageFormatException,提示“试图加载格式不正确的程序”。

原因:SQLite.Interop.dll 是原生 DLL,位数必须和宿主进程完全一致。最常见的场景是把 x86 的 Interop.dll 复制到 64 位 Windows 服务的输出目录,或者把 x64 包放在 32 位应用池里。还有一种是 VS2008 里平台目标被队友改回了 AnyCPU,在 64 位机器上以 x64 进程运行,引用的却是 x86 包,加载原生库自然是“格式不正确”。

解决:项目平台目标统一为 x64,IIS 应用池的“启用 32 位应用程序”务必设为 False。再检查发布目录里x64\SQLite.Interop.dll是否存在并且位数正确。可以写一个自检代码在运行时输出Environment.Is64BitProcess,先确认进程位数,再去对 DLL 位数。

4.2 SQLite.Interop.dll 没被复制到输出目录,报错只在运行时不报错

现象:本地 Debug 跑得好好的,发布到服务器后,在用到数据库的页面上报“未能加载文件或程序集 SQLite.Interop.dll”,或者直接 DllNotFoundException。

原因:SQLite.Interop.dll 在工程里通常是 Content 文件。VS2008 默认不会把子目录里的内容文件自动复制到输出目录。你开发机上因为手工复制过,所以没暴露,一换机器全现原形。

解决:在 .csproj 中给 x64 和 x86 两个 Interop 文件都加上复制标记:

<Content Include="x64\SQLite.Interop.dll"> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> </Content> <Content Include="x86\SQLite.Interop.dll"> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> </Content>

PreserveNewest表示只有源文件比目标文件新时才复制,既避免每次编译全量拷贝,又保证更新时一定能同步。这种做法会让发布目录同时存在 x64 和 x86 两个原生库,体积多了几百 KB,但换来的是目标机器上不需要手工干预。

4.3 数据库文件“建不出来”或“跑错路径”

现象:程序没报错,但死活找不到数据库文件;或者找到了文件,内容却是空库。还有一种表现是 Windows 服务启动时报“unable to open database file”,但本地控制台跑就正常。

原因:连接字符串里写了相对路径Data Source=app.db,这里的相对路径是相对于进程当前工作目录。IIS 工作进程的当前目录通常在System32\inetsrv,Windows 服务则取决于启动方式,不是你想象的应用根目录。于是 SQLite 把文件建到了系统目录里,权限不足时直接报无法打开。

解决:不要给 SQLite 喂相对路径,用程序集所在目录或专门的数据库目录拼绝对路径:

string dbDir = Path.Combine( AppDomain.CurrentDomain.BaseDirectory, "data"); Directory.CreateDirectory(dbDir); string connStr = "Data Source=" + Path.Combine(dbDir, "app.db") + ";Version=3;";

AppDomain.CurrentDomain.BaseDirectory在服务和 Web 程序里都指向应用目录,比Environment.CurrentDirectory稳定得多。Directory.CreateDirectory保险,因为 SQLite 不会自动创建不存在的目录。

4.4 DbProviderFactories 配置无效或找错 invariant

现象:调用DbProviderFactories.GetFactory("System.Data.SQLite")时抛异常,提示找不到指定的 provider,但配置文件里明明有 add 节点。

原因:type 字段里的 Version 或 PublicKeyToken 和实际加载的程序集不一致。1.0.106.0 这个版本在强名称程序集里,公钥标记是固定的,一旦你从网上复制了一段别的版本的配置,类型加载就会失败。还有一种可能是大小写不匹配,invariant 字符串必须完全一致。

解决:不要手填公钥标记,写几行代码读出来再填进配置:

using System; using System.Reflection; using System.Data.SQLite; byte[] pk = typeof(SQLiteConnection).Assembly .GetName() .GetPublicKeyToken(); Console.WriteLine(BitConverter .ToString(pk) .Replace("-", "") .ToLower());

拿到的十六进制字符串就是 PublicKeyToken。如果项目里没有直接用 DbProviderFactory,我更建议这条路:把 providerFactories 配置整个去掉,直接 new SQLiteConnection,少一个配置点就少一类翻车可能性。

4.5 并发写锁:“database is locked” 不是玄学

现象:多个线程或多个进程同时对同一个库文件写入时,报“database is locked”,甚至在只读操作时也偶发。

原因:SQLite 的锁是文件级锁,写事务未提交时,其他连接写会被阻塞。默认 busy timeout 只有 5 秒,超时后直接报错。很多老代码在using外用了静态连接,没有及时关闭或提交,导致锁一直被持有。

解决:第一优先是给连接串加Default Timeout=30,延长等待窗口。第二是保证每个连接都是短生命周期,用完即丢,连接池负责复用。第三是如果并发读多写少,可以打开 WAL 模式,读写并发能提高不少:

using (SQLiteCommand cmd = conn.CreateCommand()) { cmd.CommandText = "PRAGMA journal_mode=WAL;"; cmd.ExecuteNonQuery(); }

WAL 模式让读操作不再阻塞写事务,写与写仍然串行;这是让老系统并发能力提升性价比很高的一步。要注意的是,WAL 会生成 .db-wal 和 .db-shm 两个额外文件,做文件备份时不能只复制 .db 一个文件,这一点很多运维会忽略。

5. 给老项目留一个自检函数,别让 SQLite 变成黑匣子

这个版本在老服务器上出问题时,用户报上来的话往往是“程序跑不起来”“库打不开”,信息量很低。与其远程一顿瞎猜,不如在项目里留一个自检函数,部署到目标机器后先运行一遍,把关键信息直接打出来。

public static bool SelfCheck(string dbPath, out string log) { StringBuilder sb = new StringBuilder(); try { sb.AppendLine("进程位数: " + (IntPtr.Size == 8 ? "x64" : "x86")); Assembly asm = typeof(SQLiteConnection).Assembly; sb.AppendLine("驱动版本: " + asm.GetName().Version); string connStr = "Data Source=" + dbPath + ";Version=3;"; using (SQLiteConnection conn = new SQLiteConnection(connStr)) { conn.Open(); using (SQLiteCommand cmd = conn.CreateCommand()) { cmd.CommandText = "PRAGMA integrity_check; " + "CREATE TABLE IF NOT EXISTS _self_check(" + "id INTEGER PRIMARY KEY);"; cmd.ExecuteNonQuery(); cmd.CommandText = "SELECT SQLITE_VERSION();"; sb.AppendLine("引擎版本: " + cmd.ExecuteScalar()); } } log = sb.ToString(); return true; } catch (Exception ex) { log = sb.ToString() + "异常: " + ex.Message; return false; } }

PRAGMA integrity_check会对整个数据库做完整性校验,正常情况下会返回 ok,如果返回其他内容,说明库文件已经有物理损坏,这比业务报错早一步暴露问题。CREATE TABLE IF NOT EXISTS _self_check是为了验证写入权限和文件锁是否正常,能执行成功说明不是只读或磁盘权限问题。这个函数不放在业务主流程里,而是做成一个小工具入口,上线或排障时单独调用。

我第一次在客户 64 位服务器上排查“database is locked”,远程折腾了一个多小时,后来让现场同事跑了一遍这个自检,日志显示进程是 x86,安装包里的 Interop.dll 是 x64,位数错配导致锁和加载行为全乱。从那以后我养成了习惯:任何 SQLite 相关的老项目维护,先跑自检,再动业务代码。省下来的时间比写这个函数多得多。希望这套方法能帮到你,也少踩那几个黑匣子。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/13 1:23:46

AI芯片软硬件协同设计:从架构探索到性能调优的工程实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/13 1:23:45

STM32零硬件入门:Keil仿真跑通GPIO、定时器与串口

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/13 1:22:33

STM32端口复用与重映射详解:AF寄存器配置与F1/F4差异

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/13 1:22:33

Arduino墨水屏智能手表DIY:低功耗开源方案实现30天续航

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/13 1:22:30

MinGW-w64 下 vlc-qt 预编译包集成实战:Qt 内嵌 VLC 播放器避坑指南

简介&#xff1a;本资源为基于 Qt5.15.2 与 VLC3.0.14 的 VLC-Qt 集成编译安装包&#xff0c;采用 mingw8.1.0_64 工具链在 Windows 64 位环境下构建&#xff0c;面向需要在 Qt 项目中嵌入 VLC 播放能力的开发者与爱好者&#xff0c;可省去自行编译 VLC-Qt 的繁琐过程&#xff…

作者头像 李华
网站建设 2026/10/13 1:22:05

TensorFlow CNN深度解析:从MNIST看懂卷积层内存布局与梯度流

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华