ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

VS2012下SQLite加密实战:System.Data.SQLite增删改查与避坑指南

VS2012下SQLite加密实战:System.Data.SQLite增删改查与避坑指南

简介:本资源面向在64位Windows平台使用C# 2012与VS2012开发数据驱动应用的开发者,重点解决SQLite数据库的调用与密码保护问题。内容围绕System.Data.SQLite提供程序展开,涵盖连接字符串配置、建库建表、增删改查及带密码连接等核心操作,适合具备一定C#基础、希望掌握轻量级嵌入式数据库安全管理的读者参考。压缩包共42个文件,约2.48MB,以cs源码、config配置、dll程序集、resx资源、exe可执行文件及sln解决方案为主,另含pdb调试符号与suo用户选项等辅助文件,结构完整可直接编译运行。目前已有851人学习下载。通过研读源码,读者可理解SQLite在64位环境下的集成方式、密码参数在连接字符串中的实际作用,以及数据表创建、参数化插入与DataReader读取的完整实现思路,为开发跨平台、轻量级且具备安全特性的数据应用提供可复用的参考范例。

1. 为什么还有人用 VS2012 配 SQLite 做本地库

前阵子帮一个做工业上位机的朋友看代码,他的项目还在用 VS2012 + .NET Framework 4.0,数据库选的是 SQLite。我第一反应是"这组合也太老了吧",但看完他的部署环境就闭嘴了——现场工控机跑的是 Windows XP Embedded,装不了新版 .NET,也带不动 LocalDB。这种场景下,SQLite 几乎是唯一解:单文件、零配置、不需要独立服务进程,拷过去就能跑。

这份源码包解决的就是这个具体问题:在 VS2012(也就是 C# 2012 语法环境)下,用 System.Data.SQLite 这个托管驱动,完成数据库的创建、增删改查,并且给库文件设置密码。它适合两类人:一是维护老工控/医疗/收银系统的开发者,二是想搞清楚 SQLite 加密到底怎么落地的新手。下面我按"驱动怎么选 → 连接串怎么写 → 密码怎么设 → 坑在哪"的顺序拆一遍。

2. 驱动选型与工程搭建:System.Data.SQLite 的版本对应关系

2.1 为什么不用 Microsoft.Data.Sqlite

现在网上搜 C# SQLite,一大半文章推的是 Microsoft.Data.Sqlite。但这个包最低要求 .NET Standard 2.0,对应到 .NET Framework 至少得 4.6.1。VS2012 默认新建的工程是 .NET Framework 4.0 或 4.5,根本引用不了。所以在 VS2012 环境下,唯一现实的选择是 System.Data.SQLite,也就是 SQLite 官方团队维护的那个 ADO.NET 驱动。

它有几个关键特性需要先搞清楚:

  • 它是混合程序集,里面既有托管代码(System.Data.SQLite.dll),也有非托管代码(SQLite.Interop.dll)。托管部分负责 ADO.NET 接口,非托管部分才是真正的 SQLite 引擎。
  • 加密功能不是 SQLite 原生自带的,是驱动在编译时加进去的扩展。所以设密码这个能力,取决于你用的驱动版本是否带加密支持。
  • 32 位和 64 位是分开的。SQLite.Interop.dll 分 x86 和 x64 两个版本,选错了会在运行时抛Unable to load DLL 'SQLite.Interop.dll'。

2.2 工程引用的正确姿势

在 VS2012 里新建一个 Windows 窗体应用或者控制台应用,然后按下面的步骤操作:

# 假设你把源码包解压到了 D:\SQLiteDemo # 目录结构大概是这样的: # D:\SQLiteDemo\ # ├── lib\ # │ ├── System.Data.SQLite.dll # │ ├── x86\SQLite.Interop.dll # │ └── x64\SQLite.Interop.dll # ├── src\ # │ └── SQLiteDemo.csproj # └── db\ # └── test.db

引用 System.Data.SQLite.dll 之后,必须手动处理 Interop 文件。很多人以为引用了 DLL 就完事了,结果一运行就报错。正确做法是在项目文件里加一段 MSBuild 配置,让编译时自动把对应平台的 Interop 拷到输出目录:

<!-- 在 .csproj 的 </Project> 之前插入 --> <ItemGroup Condition="'$(Platform)' == 'x86'"> <Content Include="lib\x86\SQLite.Interop.dll"> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> </Content> </ItemGroup> <ItemGroup Condition="'$(Platform)' == 'x64'"> <Content Include="lib\x64\SQLite.Interop.dll"> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> </Content> </ItemGroup>

这段配置的逻辑是:根据当前编译平台,把对应的 Interop 文件标记为"内容",并设置"较新则复制"。参数说明:Condition判断平台,CopyToOutputDirectory的PreserveNewest表示源文件比目标新时才覆盖,避免每次全量拷贝拖慢编译。

提示:如果你的工程是 AnyCPU,建议改成 x86 或 x64 明确指定。AnyCPU 在 64 位系统上会以 64 位进程运行,但如果你只放了 x86 的 Interop,照样报错。

2.3 连接字符串的写法

连接字符串决定了数据库文件路径、密码、是否只读等行为。基础写法:

// 拼接连接字符串,注意 Data Source 的路径要用绝对路径或相对于工作目录 string dbPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "db", "test.db"); SQLiteConnectionStringBuilder builder = new SQLiteConnectionStringBuilder(); builder.DataSource = dbPath; builder.Password = "MySecret123"; // 设置密码 builder.FailIfMissing = false; // 文件不存在时自动创建 builder.SyncMode = SynchronizationModes.Normal; // 同步模式,兼顾性能与安全 string connStr = builder.ConnectionString;

用SQLiteConnectionStringBuilder而不是手拼字符串,好处是它会自动处理转义和格式。参数说明:Password就是加密密钥,FailIfMissing设为 false 时,如果文件不存在会自动新建一个空库;SyncMode控制写入时是否立即刷盘,Normal 在大多数场景够用,Full 更安全但更慢。

3. 建库、设密码与增删改查的完整流程

3.1 第一次创建带密码的数据库

这里有个容易翻车的点:你不能先建一个无密码的库,然后再"改密码"。System.Data.SQLite 的加密是在创建文件时就写入头部的,后续修改需要走ChangePassword方法,但那个方法对已有数据的库操作有风险。稳妥做法是第一次就用带密码的连接串创建。

// 创建带密码的数据库并建表 public static void CreateEncryptedDatabase(string dbPath, string password) { // 如果文件已存在,先删掉,保证从零开始 if (File.Exists(dbPath)) { File.Delete(dbPath); } SQLiteConnectionStringBuilder builder = new SQLiteConnectionStringBuilder(); builder.DataSource = dbPath; builder.Password = password; builder.FailIfMissing = false; using (SQLiteConnection conn = new SQLiteConnection(builder.ConnectionString)) { conn.Open(); using (SQLiteCommand cmd = conn.CreateCommand()) { // 建一张用户表,主键自增 cmd.CommandText = @"CREATE TABLE IF NOT EXISTS Users ( Id INTEGER PRIMARY KEY AUTOINCREMENT, Name TEXT NOT NULL, Age INTEGER, CreatedAt DATETIME DEFAULT CURRENT_TIMESTAMP )"; cmd.ExecuteNonQuery(); } } }

逻辑说明:先删旧文件是为了演示干净流程,实际项目里要加判断。FailIfMissing = false配合Password设置,驱动会在创建文件时用指定密钥加密。建表语句里AUTOINCREMENT保证 Id 单调递增,DEFAULT CURRENT_TIMESTAMP让插入时自动填时间。

3.2 插入与查询:参数化查询防注入

老代码里经常看到字符串拼接 SQL,这在带密码的库上一样危险。参数化查询是底线:

// 插入一条记录,返回自增 Id public static long InsertUser(string dbPath, string password, string name, int age) { SQLiteConnectionStringBuilder builder = new SQLiteConnectionStringBuilder(); builder.DataSource = dbPath; builder.Password = password; using (SQLiteConnection conn = new SQLiteConnection(builder.ConnectionString)) { conn.Open(); using (SQLiteCommand cmd = conn.CreateCommand()) { cmd.CommandText = "INSERT INTO Users (Name, Age) VALUES (@name, @age); SELECT last_insert_rowid();"; // 参数化,避免 SQL 注入 cmd.Parameters.AddWithValue("@name", name); cmd.Parameters.AddWithValue("@age", age); // ExecuteScalar 返回第一行第一列,这里就是自增 Id object result = cmd.ExecuteScalar(); return Convert.ToInt64(result); } } }

参数说明:@name和@age是占位符,AddWithValue会自动推断类型。SELECT last_insert_rowid()是 SQLite 的内置函数,返回当前连接最后一次插入的自增 Id。注意必须和 INSERT 在同一个 CommandText 里执行,分开执行会拿到错误的值。

查询操作类似,用SQLiteDataReader逐行读:

// 查询所有用户,返回 DataTable 方便绑定到界面 public static DataTable QueryUsers(string dbPath, string password) { SQLiteConnectionStringBuilder builder = new SQLiteConnectionStringBuilder(); builder.DataSource = dbPath; builder.Password = password; DataTable table = new DataTable(); using (SQLiteConnection conn = new SQLiteConnection(builder.ConnectionString)) { conn.Open(); using (SQLiteCommand cmd = conn.CreateCommand()) { cmd.CommandText = "SELECT Id, Name, Age, CreatedAt FROM Users ORDER BY Id DESC"; using (SQLiteDataAdapter adapter = new SQLiteDataAdapter(cmd)) { adapter.Fill(table); // 自动填充 DataTable } } } return table; }

SQLiteDataAdapter是 ADO.NET 的标准适配器,Fill方法会把结果集映射成 DataTable 的行列结构。这种写法在 WinForm 里直接绑 DataGridView 很方便,不需要手动逐行读。

3.3 修改密码与压缩数据库

如果确实需要改密码,System.Data.SQLite 提供了ChangePassword方法:

// 把数据库密码从 oldPwd 改成 newPwd public static void ChangeDatabasePassword(string dbPath, string oldPwd, string newPwd) { SQLiteConnectionStringBuilder builder = new SQLiteConnectionStringBuilder(); builder.DataSource = dbPath; builder.Password = oldPwd; using (SQLiteConnection conn = new SQLiteConnection(builder.ConnectionString)) { conn.Open(); conn.ChangePassword(newPwd); // 关键调用 } }

逻辑说明:ChangePassword会重新加密整个数据库文件,数据量大的时候会阻塞。建议在业务低峰期执行,并且提前备份。参数就是新旧密码,没有其他可选配置。

另外,SQLite 删除数据后文件不会自动缩小,需要执行VACUUM:

// 压缩数据库,回收已删除数据占用的空间 public static void VacuumDatabase(string dbPath, string password) { SQLiteConnectionStringBuilder builder = new SQLiteConnectionStringBuilder(); builder.DataSource = dbPath; builder.Password = password; using (SQLiteConnection conn = new SQLiteConnection(builder.ConnectionString)) { conn.Open(); using (SQLiteCommand cmd = conn.CreateCommand()) { cmd.CommandText = "VACUUM;"; cmd.ExecuteNonQuery(); } } }

VACUUM会重建整个数据库文件,所以执行期间需要额外的磁盘空间,大概是原库大小的两倍。带密码的库执行 VACUUM 后密码保持不变,不需要重新设置。

4. 避坑排查:密码、平台与部署的五个血泪教训

4.1 报错 "file is encrypted or is not a database"

现象:用无密码的连接串去打开一个带密码的库,或者密码输错,直接抛这个异常。

原因:SQLite 加密库的文件头是加密后的,驱动用错误密钥解密时读到的头部不是合法的 "SQLite format 3" 标识。

解决:确认连接串里的Password和创建时一致。如果忘了密码,基本没有后悔药,只能从备份恢复。所以生产环境一定要把密码存在配置文件里并做好备份。

4.2 报错 "Unable to load DLL 'SQLite.Interop.dll'"

现象:编译通过,运行时报找不到 Interop。

原因:前面说过,Interop 分平台,且不会被自动拷贝到输出目录。

解决:按 2.2 节的 MSBuild 配置处理,或者手动把对应平台的 SQLite.Interop.dll 放到 bin\Debug 或 bin\Release 下。用 Process Monitor 看进程到底在哪个路径找这个 DLL,是最快的定位方式。

4.3 在 64 位系统上以 AnyCPU 运行报错

现象:开发机是 64 位,编译成 AnyCPU,运行时以 64 位进程启动,但只部署了 x86 的 Interop。

原因:AnyCPU 在 64 位系统上默认走 64 位,加载 x86 的 Interop 会失败。

解决:要么把工程目标平台固定为 x86,要么同时部署两个平台的 Interop 并在配置里做条件拷贝。我一般直接固定 x86,因为很多老工控环境就是 32 位。

4.4 密码设置后,用其他 SQLite 工具打不开

现象:用 SQLite Expert 或 DB Browser 打开带密码的库,提示无法识别。

原因:System.Data.SQLite 的加密是它自己的扩展,不是 SQLite 标准加密。其他工具如果不支持同样的加密算法,就打不开。

解决:这是预期行为。如果需要在多个工具间共享,要么统一用 System.Data.SQLite,要么不用密码改用文件系统权限控制。选型时就要想清楚。

4.5 并发写入时报 "database is locked"

现象:多个线程或进程同时写同一个库,抛 database is locked。

原因:SQLite 默认是库级锁,同一时刻只允许一个写入者。带密码的库同样受这个限制。

解决:写入操作串行化,或者用busy_timeout参数让驱动自动重试:

// 设置忙等待超时,单位毫秒 builder.BusyTimeout = 5000;

BusyTimeout表示遇到锁时最多等 5 秒再报错。对于写入不频繁的上位机场景,这个值够用。如果写入非常频繁,应该考虑换用客户端-服务器型数据库,而不是硬扛 SQLite。

5. 进阶技巧:用事务批量插入把性能拉满

单条 INSERT 在 SQLite 里每次都要刷盘,一万条数据可能要几十秒。用事务包起来能快一到两个数量级。下面这个批量插入方法是我在工控项目里常用的:

// 批量插入用户,用事务包裹,性能提升明显 public static void BatchInsertUsers(string dbPath, string password, List<Tuple<string, int>> users) { SQLiteConnectionStringBuilder builder = new SQLiteConnectionStringBuilder(); builder.DataSource = dbPath; builder.Password = password; builder.SyncMode = SynchronizationModes.Normal; // 事务内用 Normal 即可 using (SQLiteConnection conn = new SQLiteConnection(builder.ConnectionString)) { conn.Open(); using (SQLiteTransaction trans = conn.BeginTransaction()) { using (SQLiteCommand cmd = conn.CreateCommand()) { cmd.CommandText = "INSERT INTO Users (Name, Age) VALUES (@name, @age)"; // 预定义参数,循环里只改值,避免重复解析 SQL SQLiteParameter pName = cmd.Parameters.Add("@name", DbType.String); SQLiteParameter pAge = cmd.Parameters.Add("@age", DbType.Int32); foreach (var user in users) { pName.Value = user.Item1; pAge.Value = user.Item2; cmd.ExecuteNonQuery(); } } trans.Commit(); // 一次性提交 } } }

逻辑说明:BeginTransaction开启事务后,所有写入先落在日志里,Commit时才真正刷盘。参数用Add预定义而不是循环里AddWithValue,避免每次重新解析 SQL 语句。实测一万条插入从 40 秒降到 1 秒以内。

验证方法:插入完成后用QueryUsers查总数,或者直接看 db 文件大小变化。如果事务中途抛异常,using块会自动回滚,不会留下半截数据。

注意:事务不要开太大,一次几万条就够了。超大事务会占用大量内存,而且失败时回滚代价高。

从那以后我每次给老项目做 SQLite 优化,都强制先看三件事:Interop 平台对不对、连接串密码有没有、批量操作有没有包事务。这三条过了,基本不会翻车。希望帮到你。

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

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进