ARTICLE DETAIL

资讯详情

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

.NET老项目集成SQLite:引用库部署与32/64位避坑指南

.NET老项目集成SQLite:引用库部署与32/64位避坑指南 简介面向.NET Framework 4.0开发的SQLite连接引用库压缩包同时提供32位x86与64位x64两套二进制程序集帮助.NET项目解决Windows平台下引用System.Data.SQLite时的位数匹配与运行时依赖问题。包内共50个文件以核心System.Data.SQLite.dll为主同时包含LINQ和EF6扩展程序集、XML注释文档、config配置文件、PDB调试符号以及可执行的测试程序另有Northwind示例数据库整体大小4.26MB目录按x86与x64清晰分置。开发者可依据目标架构选择对应目录直接引用后即可使用ADO.NET接口完成建表、增删改查、事务与异步操作也能配合Entity Framework采用Code First模式简化数据访问层。目前已有441人学习浏览适合需要快速在.NET桌面应用或轻量级工具中集成SQLite的初中级开发者。1. 还在维护的老系统绕不开这份 2010 年的 SQLite 引用库在 .NET Framework 4.0 时代想在 WinForm、WebForms 或 Windows 服务里用 SQLite最稳的方案就是 sqlite-netFx40 这样一份把托管驱动和原生引擎打包在一起的引用库而且里面会同时给出 32 位和 64 位程序各自需要的原生部件。它解决的问题到今天依然存在老项目不能随便升框架客户机器上也没有乱七八糟的新运行时能找到一个不依赖新框架、开箱即用的 SQLite 驱动比什么都重要。这个 rar 适合的人群很明确正在维护旧代码、手头只有离线环境、又被各种 DLL 加载报错折磨过的 .NET 工程师。如果你接手的就是这样一个老系统这份笔记可以帮你把引用选对、位数放对、坑踩平。2. 先认全 rar 里的文件System.Data.SQLite 到底靠几个 DLL 干活2.1 两个 DLL 的分工托管入口与原生引擎这套引用模型的基础是 System.Data.SQLite.dll 和 SQLite.Interop.dll 两个文件。System.Data.SQLite.dll 是 C# 写的 ADO.NET 提供程序对外暴露 SQLiteConnection、SQLiteCommand、SQLiteDataReader 这些类型让你可以像用 SqlClient 一样写 SQLite 代码。它本身不包含任何 SQL 引擎真正的数据库内核在 SQLite.Interop.dll 里这个原生 DLL 内部编译进了完整的 sqlite3 库。你写的new SQLiteConnection(...)只是把请求交到托管层托管层通过 P/Invoke 把 SQL 语句传给原生引擎由原生引擎去打开文件、执行索引扫描、写事务日志最后把结果集逐行还给你。如果你在项目里只添加了 System.Data.SQLite.dll 的引用编译不会报错一运行就会暴露问题找不到原生库。这是很多人第一次接触这套 rar 时最容易踩的坑因为“引用”这个概念在托管层面非常好用但在混合模式程序集面前原生文件没法靠“添加引用”来管理只能靠部署到正确的位置。把两个文件拆开还有一个实际后果发布包会多出几个文件但换来的是位数切换时互不干扰——x86 进程拿 x86 的原生库x64 进程拿 x64 的原生库谁也碍不着谁。常见做法是把 System.Data.SQLite.dll 放在程序根目录SQLite.Interop.dll 按位数放进 bin\x64 或 bin\x86 子目录运行时按进程位数去找对应的原生库。这个约定和后来 .NET Core 的 runtimes 目录很像但本质是手工约定不是框架强制规则。你放错位置它不会自动去别处找直接抛 DllNotFoundException。2.2 32 位与 64 位的文件选择不是看操作系统位数是看进程位数很多人在下载选型时习惯问“我系统是 64 位的是不是只拿 x64 文件就行”这个问题很容易把方向带偏。真正决定文件选择的是 .NET 进程的位数你的程序编译成 x86就算跑在 64 位系统上进程依然是 32 位加载 64 位原生库一定会撞坏程序编译成 x64就只能加载 64 位原生库。操作系统位数只决定这台机器能不能运行某个位数的进程不决定进程内部能加载哪个文件。解开 rar 之后最常见的情况是里面按目录结构分好了 x86 和 x64 两个子目录两个目录里都有同名或对应的原生库只是二进制平台不同。选择的原则很简单项目目标平台是 x86就使用 x86 那套目标平台是 x64就使用 x64 那套。放的时候也建议保持这套目录约定程序目标平台需要部署的原生文件输出目录里的位置x86x86 目录下的 SQLite.Interop.dllapp.exe 同级放进 x86 子目录x64x64 目录下的 SQLite.Interop.dllapp.exe 同级放进 x64 子目录托管 DLLSystem.Data.SQLite.dllapp.exe 同级根目录这个约定不是 Windows 的标准而是 System.Data.SQLite 发布版一直沿用的部署习惯。托管 DLL 通常是 AnyCPU 编译的放根目录谁都能用原生 DLL 带平台信息必须按位数各回各家。如果 rar 里还散着一个 sqlite3.dll那是给愿意自己写 P/Invoke 绕开 ADO.NET 的同学准备的对走 System.Data.SQLite 的普通业务代码可以忽略它们两个不是同一个东西别在部署时顺手把这个文件覆盖到 SQLite.Interop.dll 的位置上。发布目录里只要保证根目录有 System.Data.SQLite.dll、x86/x64 下有对应的 SQLite.Interop.dll就是这套方案预期的最终形态多余文件一概不发布。2.3 用 C# 打开 sqlite 数据库的最小代码写代码前要明白一件事这个 rar 里的驱动是 ADO.NET 提供程序不是 ORM里面没有 EF 之类的封装所以你会看到连接对象、命令对象、阅读器对象这些老熟人。引用配好之后写代码反而是最简单的一步。下面是一个最小可运行的 C# 示例打开或创建 app.db建一张表并插入一行using System; using System.Data.SQLite; class SqliteDemo { static void Main() { // Version3 表示文件按 SQLite 3 格式读写老驱动必须写这个参数 string connStr Data Sourceapp.db;Version3;PoolingTrue;; using (var conn new SQLiteConnection(connStr)) { conn.Open(); // 如果 app.db 不存在会自动创建空的数据库文件 using (var cmd new SQLiteCommand( CREATE TABLE IF NOT EXISTS t_log( id INTEGER PRIMARY KEY AUTOINCREMENT, message TEXT, created_at TEXT), conn)) { cmd.ExecuteNonQuery(); } using (var insert new SQLiteCommand( INSERT INTO t_log(message, created_at) VALUES(msg, time), conn)) { insert.Parameters.AddWithValue(msg, hello); insert.Parameters.AddWithValue(time, DateTime.Now.ToString(yyyy-MM-dd HH:mm:ss.fff)); insert.ExecuteNonQuery(); } } } }这里的连接字符串值得解释一下Data Source 指向数据库文件路径支持相对路径和绝对路径Version3 是 System.Data.SQLite 的老式约定告诉驱动按 SQLite 3 文件格式工作别省略否则某些老版本解析字符串会出问题PoolingTrue 让物理连接复用起来省去反复开关文件句柄的开销。参数化方式与 SqlClient 几乎一致区别是 SQLite 的参数名直接带 前缀。如果这段代码能正常走到最后一行不抛异常说明你的引用和位数部署已经成功了一半下一节讲怎么把这些文件接进真实项目。3. 把引用接进 .NET Framework 项目手动引用与 config 配置3.1 离线解包后手动引用的完整步骤拿到 rar 之后第一件事不是双击运行而是确认解压后的目录结构。我的习惯是先建一个libs\sqlite目录把 System.Data.SQLite.dll 和 x86、x64 两个文件夹一起放进去整个目录随代码走、进版本库。这样团队成员拉代码后不需要各自去下一个包也不会出现“我本地能跑到你那就报错”的玄学问题。然后在 Visual Studio 里按下面几步操作在解决方案资源管理器里右键“引用”选择“添加引用”浏览到 libs 目录里的 System.Data.SQLite.dll加上引用后把“复制本地”设为 true。不要把 SQLite.Interop.dll 加进“引用”列表它是原生库加进去要么被 Visual Studio 当普通程序集解析要求补强名称要么只是被复制成裸文件运行时仍然不会被 CLR 当作程序集加载。打开项目的 csproj确认 TargetFrameworkVersion 是 v4.0 或以上再把 PlatformTarget 显式写成 x86 或 x64别留着 AnyCPU 糊里糊涂编译。在项目目录里建 x86、x64 两个子文件夹把对应位数的 SQLite.Interop.dll 分别放进去并把这两个文件的“复制到输出目录”设为“始终复制”。编译后到 bin 目录看一眼app.exe 和 System.Data.SQLite.dll 在根目录SQLite.Interop.dll 在 x86 或 x64 子目录里这个结构对了再开始跑。第二步容易引起困惑单独说明一下原生 DLL 没有“引用”的语义运行时靠的是文件系统路径搜索不是 .NET 程序集加载。正确做法是把它当“内容文件”对待放对目录、设好复制属性让每次生成都自动把文件带过去。“始终复制”和“如果较新则复制”在老项目里差别很大如果你手工更新了 libs 里的原生文件但忘记加上较新标记后一种模式可能不复制导致运行用的还是旧文件我一般建议原生库这类文件统一选“始终复制”省得在这种地方省出一个半夜排查的活。下面是 csproj 里关键平台节点的示例直接看这几个配置就知道项目是不是干净状态PropertyGroup OutputTypeWinExe/OutputType TargetFrameworkVersionv4.0/TargetFrameworkVersion PlatformTargetx86/PlatformTarget /PropertyGroup ItemGroup Reference IncludeSystem.Data.SQLite HintPathlibs\sqlite\System.Data.SQLite.dll/HintPath PrivateTrue/Private /Reference /ItemGroupPlatformTarget 节点是这一节最不能被省掉的配置。如果你公司在 64 位服务器上统一用 x86 跑老程序那 x86 项目装 x64 系统完全正常但别让 VS 自动改成 AnyCPU否则后续 64 位进程中即使配好了 SQLite.Interop.dll也可能因为托管层加载顺序不同再次踩到位数坑。3.2 为什么 NuGet 不是首选离线 rar 的适用边界现在新建项目很容易直接Install-Package System.Data.SQLite.CoreNuGet 会把托管 DLL 和原生运行库一起拉下来省掉很多手工活。但这有一个大前提你所在的网络能访问 NuGet 源而且你愿意接受新版本依赖的 VC 运行库和框架版本要求。对很多老项目来说这两个前提都不成立。标题里的 sqlite-netFx40-2010.rar 就是典型的离线分发方案它不需要联网、不需要额外装运行库、版本固定不会再变适合放进公司内部共享目录或者制品库随时取用。反过来也要泼一盆冷水这套 2010 年的东西只属于 .NET Framework 老项目。如果你今天用 .NET Core 或 .NET 5/6/7/8 写新组件特别是 .NET MAUI 这类跨平台界面请直接使用 Microsoft.Data.Sqlite而不是把这个 rar 拖进去。老引用库里的 System.Data.SQLite.dll 按 .NET Framework 的 API 表面设计跑到新框架上可能连类型加载阶段就报错甚至让你误以为缺少 .NET Desktop Runtime——实际不是缺运行时是库与框架不兼容。新世界有新世界的方案老 rar 不给新项目续命。它真正发光发热的地方是你我手上那些还没死、又没法大刀阔斧改框架的老系统。3.3 providerName、DbProviderFactories 与连接工厂的配置如果你的代码里直接用 SQLiteConnection 类什么都不用配置。但老项目中常常有另一套玩法通过 DbProviderFactories 注册 SQLite 提供程序再用 DbProviderFactory 统一创建连接。这种抽象层一旦用起来web.config 或 app.config 里就必须有对应配置而且很容易因为 type 字符串写错而彻底断掉。正确步骤是先把要注册的 DLL 用 PowerShell 读出强名称Add-Type -Path C:\libs\sqlite\System.Data.SQLite.dll [System.Data.SQLite.SQLiteFactory].Assembly.FullName它会打印类似System.Data.SQLite, Version..., Cultureneutral, PublicKeyTokendb937bc2d44ff139的完整名称然后把输出填进配置里的 type 字段configuration system.data DbProviderFactories remove invariantSystem.Data.SQLite / add nameSQLite Data Provider invariantSystem.Data.SQLite description.NET Framework Data Provider for SQLite typeSystem.Data.SQLite.SQLiteFactory, System.Data.SQLite, Version你的实际版本, Cultureneutral, PublicKeyTokendb937bc2d44ff139 / /DbProviderFactories /system.data /configuration其中 remove 节点不是多此一举。同一个进程里如果先加载了别的 SQLite 驱动或者 app.config 与 machine.config 重复注册同名 invariant就会抛 provider 相关的冲突异常。先 remove 再 add能保证你这条注册生效。注意 version 字符串必须与实际引用的 DLL 完全一致这是这套配置里最常见的翻车点很多人抄了一段网上配置却忘了改成自己实际引用的版本号运行时工厂创建直接失败日志里又只留一句干巴巴的“无法找到请求的 .NET Framework Data Provider”——其实问题就在 type 字符串上。4. 连接字符串与 32/64 位命中为什么同一个库能卡住整条连接4.1 “试图加载格式不正确的程序”到底在说什么编译过了、文件也放进去了一启动却抛 BadImageFormatException提示“试图加载格式不正确的程序”。这个异常几乎只指向同一个原因CLR 尝试加载一个与当前进程位数不匹配的原生模块。比如 x86 进程加载了 x64 的 SQLite.Interop.dll或者反过来。遇到这个异常先判断进程位数再判断 DLL 位数。进程位数最简单的判断方法是在程序启动入口打一行日志Console.WriteLine(进程位数: (Environment.Is64BitProcess ? x64 : x86)); Console.WriteLine(操作系统位数: (Environment.Is64BitOperatingSystem ? x64 : x86));如果进程位数跟你的 PlatformTarget 对不上说明编译配置没生效回看 csproj 的 PlatformTarget如果进程位数是对的但 SQLite.Interop.dll 是另一个位数的机制编译的那问题就出在把别的目录的文件复制到了程序输出目录。原生 DLL 的位数可以用 dumpbin 直接看出来在 Visual Studio 开发者命令提示符里执行dumpbin /headers SQLite.Interop.dll | findstr /i machine输出中 machine 一行写的是机器类型x86 对应的文件会显示machine (x86)x64 的会显示machine (x64)。这一行比看文件名准确得多因为不负责的打包可能会把文件改错名字但 COFF 文件头里的 machine 字段没法伪装。文件名是给人看的machine 头才是真实身份。这里还要补充一点.NET Framework 4.5 之后的 AnyCPU 默认会勾选“Prefer 32-bit”导致同一个 AnyCPU 项目在 64 位系统上运行时可能时而按 32 位进程跑、时而又按 64 位进程跑完全看入口程序集是怎么编译的。这种“薛定谔的位数”会让原生库的选择变得很痛苦所以对带原生依赖的老项目别省那几个字的配置直接锁定 x86 或 x64。4.2 连接字符串常用参数与加密支持连接字符串是 System.Data.SQLite 最容易出现“写错一个符号就通通白费”的地方。下面是老项目里最常见的几个参数按实际使用频率列出来参数常用取值作用与注意点Data Source文件路径支持相对路径和绝对路径路径里有空格时不需要额外转义Version3老驱动要求显式写不写有可能被当成旧格式处理PoolingTrue / False连接池复用句柄建议显式写别依赖默认行为Journal ModeWAL / Delete / MemoryWAL 提升并发读但会生成 -wal 和 -shm 临时文件Foreign KeysTrue / FalseSQLite 默认不开外键约束需要时在连接字符串里打开Password字符串见下方加密说明提示Journal Mode 选 WAL 后部署时别只拷主 .db 文件同目录的 -wal 和 -shm 要一起带走否则程序打开的是不完整的逻辑视图。关于“sqlite 数据库文件能否加密”标准 SQLite 文件本身不提供加密装个驱动并不能让一个明文 db 自动变成密文。System.Data.SQLite 提供了一套基于 AES 的整库加密能力一般通过 SQLiteConnection.ChangePassword 接口启用启用后的文件用普通十六进制编辑器看是乱码不提供密码则任何驱动都打不开。但这个能力在商用场景里的授权边界要自己确认不能把“能加密”和“无条件免费”划等号。如果数据只是防止手滑被拷贝这套够用如果要做合规级加密更稳的做法是业务层先加密再落盘或者换用支持透明加密的存储方案。4.3 DateTime 存储与毫秒数显示老系统最隐蔽的坑SQLite 没有独立的日期时间类型日期本质上是存成 TEXT、REAL 或 INTEGER。System.Data.SQLite 默认把 DateTime 序列化成 TEXT 格式具体字符串与连接字符串的 DateTimeFormat 属性有关。很多老系统的报表里时间一直不显示毫秒不是因为数据库没存而是 .NET 读出来时默认走 ToString()不给毫秒。要拿回完整毫秒代码里要显式格式化// 写入统一用带毫秒的 ISO 格式保证字符串排序和字典序一致 cmd.Parameters.AddWithValue(time, DateTime.Now.ToString(yyyy-MM-dd HH:mm:ss.fff)); // 读出按同格式还原避免默认 ToString() 丢掉毫秒 string raw reader[created_at] as string; DateTime dt DateTime.ParseExact(raw, yyyy-MM-dd HH:mm:ss.fff, System.Globalization.CultureInfo.InvariantCulture);这里有个值得记住的规律写入和读取的格式必须完全对称。写入用了 fff读取就要用 fff如果写入的是文化相关格式比如 2024/1/3 这种读取再用 ParseExact 指定 InvariantCulture 就会直接翻车。我见过不少报表数据对不齐的现场最后都归结到这两个不对称上。如果你要写 UPDATE 语句更新时间列参数化写法完全一致变化的只是 SQL 文本里的表和 SET 字段格式化函数不需要改。5. 避坑老库连接 SQLite 最容易翻车的 5 个现场5.1 DllNotFoundException明明引用了却找不到 SQLite.Interop.dll现象程序启动时抛“无法加载 DLL‘SQLite.Interop.dll’”但项目里明明已经引用了 System.Data.SQLite。原因SQLite.Interop.dll 是原生库不会被“添加引用”这种托管机制管理把它当普通引用加进去既不会完成正确的加载也不会在运行时被识别为可用原生库。大多数情况下是输出目录里压根没有这个文件或者有文件但放错了层级。解决确认项目里建了 x86/x64 两个子目录把对应文件放进去并且把这两个文件的“复制到输出目录”属性设为“始终复制”。编译完手动到 bin 目录确认文件确实出现在预期层级别只看 Visual Studio 里逻辑目录的图标打了勾磁盘输出才决定运行时行为。5.2 BadImageFormatException试图加载格式不正确的程序现象启动程序还没跑到业务代码就抛“试图加载格式不正确的程序”。这个报错信息很长容易被误判成程序集损坏。原因进程位数与 SQLite.Interop.dll 的 COFF machine 头不匹配。例如 x64 的进程加载了 x86 原生库或者是 x86 的进程加载了 x64 原生库。还有一种隐蔽情况System.Data.SQLite.dll 本身是 AnyCPU 编译但老项目里同时引用了别的强名称程序集CLR 的加载策略会被带偏。解决先把 PlatformTarget 显式设成 x86 或 x64 二选一不要留 AnyCPU再用 dumpbin 确认原生 DLL 的 machine 头最后把输出目录里所有 SQLite 相关文件清掉重新编译避免残留上一个位数的副本造成干扰。在 64 位系统上查问题优先怀疑进程位数而不是系统位数这一条能省下很多排查时间。5.3 DllNotFound 但文件存在搜索顺序和子目录约定现象bin 目录里确实有 SQLite.Interop.dll放在 x86 子目录中但运行还是报找不到 DLL。原因Windows 加载原生 DLL 的默认搜索顺序是应用程序目录、系统目录、Windows 目录、当前目录、PATH。如果你的程序集在根目录而原生库在子目录CLR 默认不会自动进子目录搜索。System.Data.SQLite 的一些版本会在托管代码里内置加载逻辑由托管层自己去探测 x86/x64 子目录并主动加载但 2010 年前后的版本不能一概而论有些有这套逻辑有些完全没有。解决最稳的办法是不依赖自动探测要么在程序启动最早期把对应子目录加入原生 DLL 搜索路径要么直接把当前进程需要的那个 SQLite.Interop.dll 平铺复制到 app.exe 根目录用空间换确定性。对于老版本驱动我推荐后者省得和搜索顺序赌运气。等你验证通过后再决定要不要换回子目录结构。5.4 SQLITE_NOTADB文件打不开先问是不是驱动版本太旧现象程序连接某个 .db 文件时报“file is not a database”或“SQLITE_NOTADB”但同一个文件用 DB Browser for SQLite 能正常打开表和数据都看得见。原因报错名称直指“这不是一个数据库”但实际原因没那么简单。老引擎打开新版本 SQLite 创建的文件时遇到不认识的文件格式特征或新特性就可能给出这个错误。另外数据库文件如果启用了 WAL 模式主 .db 和 -wal 文件没一起拷贝程序打开主文件也可能读到不一致状态。这两类情况在 SQLite 错误码上都被归类成“文件不是数据库”很容易被误判成文件损坏。解决先排除文件本身问题用 DB Browser for SQLite 打开目标库并执行一条简单查询能查就说明文件无损。再确认是不是缺 -wal 文件缺的话把同目录下的 -wal 一起拷过来。最后才是怀疑驱动版本用后面第 6 章的方式打印实际引擎版本对比创建这个库那台机器的 SQLite 版本差距太大就考虑升级驱动。sqlite 文件用什么打开这个问题在排查时优先用 DB Browser它比命令行工具直观得多。5.5 服务器缺 .NET Framework安装报错把锅甩给了 SQLite现象程序拷到一台干净的 Windows Server 上运行报的是“.NET Runtime”类错误而不是 sqlite 相关异常去装 .NET Framework 3.5 又卡在错误代码 0x80072f8f 或 0x80d03805然后误以为 SQLite 驱动连累了整个环境。原因sqlite-netFx40 系列按 .NET Framework 4.0 的目标面设计服务器如果只装了 3.5 CLR托管程序集根本无法加载根本轮不到 SQLite 的报错。而 3.5 的安装报错多数是离线环境连不上 Windows Update或者本地源指定不对跟数据库驱动没有关系。解决先查服务器上到底装了哪些 .NET Framework 版本决定要不要多装一套运行时。打开注册表到HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\NET Framework Setup\NDP\v4\Full看 Release 值是否有效如果没有 v4 节点就把项目目标框架和引用库都调整到与服务器匹配的版本或者通过离线安装包补齐框架。这个排查顺序能避免你抱着 sqlite 日志研究半天结果发现操作系统环境才是真凶。6. 验证与收尾确认原生库版本、加密与位数没有白配引用接好、代码能跑之后再用两个手段给这套配置做个体检能省掉上线之后的半夜电话。第一个手段是直接打印引擎版本注意这不是看 System.Data.SQLite.dll 的文件版本而是看它内部真正的 sqlite3 引擎版本对于混合程序集两者可能不一致真正决定行为的是后者using (var conn new SQLiteConnection(Data Sourceapp.db;Version3;)) { conn.Open(); using (var cmd new SQLiteCommand(SELECT sqlite_version();, conn)) { Console.WriteLine(实际引擎版本: cmd.ExecuteScalar()); } }能正常打印出版本号说明托管 DLL、原生 DLL、位数、文件位置四条链路全部打通打印出来是 3.x 的某个老版本则说明引擎确实来自 2010 年那一代遇到新库文件时的兼容问题也就符合预期了。第二个手段是用 dumpbin 复查原生 DLL 的 machine 头把位数锁定在进程位数上前面 5.2 和 5.3 的坑就不会再犯。我的个人习惯是拿到这类 rar 之后先做一个只包含连接和版本查询的独立小工程留在解决方案里以后任何人接手老代码第一件事就是跑这个小工程看版本和位数。这个工程不承担业务只负责把环境问题从业务逻辑里剥离出来排查效率能提高不少。要是你手头也有还在用 sqlite-netFx40 这类老引用库的项目不妨先把这套验证脚本建起来希望帮到你。本文还有配套的精品资源点击获取
返回列表