简介:这份C# SQLite开发包及实例源码面向需要在本机或移动端嵌入轻量数据库的.NET开发者,尤其适合刚接触SQLite、希望快速跑通增删改查与事务处理的初中级程序员。资源包共85个文件,以52个dll动态库、8个cs源码、6个exe可执行程序为主,另含config配置、resx资源、db数据库与sln解决方案等,压缩后约19.17MB,既有可直接运行的SQLiteStudio管理工具,也包含System.Data.SQLite驱动与SQLiteDBDemo示例工程,方便对照学习连接字符串、命令执行与数据绑定写法。SQLite本身无需安装、单文件存储、支持多语言接口与跨平台运行,并通过独占与共享锁实现独立事务,这些特性在示例中均有体现。目前已有952人学习下载,适合作为桌面小工具、本地缓存或Android嵌入场景的入门参考,帮助读者理解驱动引用、工程结构与基本操作流程。
1. 拿到一个 C# SQLite 开发包,先别急着双击 .sln
你从同事手里接过一个C# SQLite开发包及实例源码.zip,解压后看到一堆.cs、.csproj、.db文件,第一反应可能是找.sln双击打开、F5 跑起来。但十有八九会翻车:要么提示System.Data.SQLite版本不匹配,要么SQLite.Interop.dll找不到,要么数据库文件被锁住打不开。这个标题背后真正要解决的问题,不是「怎么新建一个 SQLite 连接」,而是「怎么把一份别人写好的 C# SQLite 开发包和实例源码,在自己的机器上完整跑通,并且能改、能扩展、能用到上位机项目里」。
它适合三类人:正在做 C# 上位机、需要本地轻量存储的工程师;想找一个能直接抄的 SQLite 增删改查封装模板的初学者;以及手里已经有一份开发包源码、但被 x86/x64 和 Interop 问题卡住的人。下面我按「先认清包里有什么 → 再跑通最小实例 → 再做封装和参数调优 → 最后避坑」的顺序讲,每一步都落到可复现的命令和代码上。
2. 拆开开发包:先分清 SQLite 在 C# 里的三种接入方式
2.1 开发包里常见的三类依赖,先对号入座
拿到C# SQLite开发包及实例源码.zip,不要急着编译。先看.csproj或packages.config里引用了什么。C# 操作 SQLite 常见有三条路线,开发包用哪条决定了你后面怎么配。
| 接入方式 | 典型引用 | 特点 | 适合场景 |
|---|---|---|---|
| System.Data.SQLite | System.Data.SQLite.dll+SQLite.Interop.dll | 官方老牌,功能全,需处理 x86/x64 | 传统 WinForm/WPF 上位机 |
| Microsoft.Data.Sqlite | NuGet 包Microsoft.Data.Sqlite | 微软维护,跨平台,依赖少 | .NET Core/.NET 6+ 新项目 |
| SQLitePCLRaw + EF Core | Microsoft.EntityFrameworkCore.Sqlite | ORM 方式,写实体类即可 | 需要 LINQ、迁移的项目 |
如果你打开实例源码看到using System.Data.SQLite;,那基本就是第一条路线。这条路线最大的特点,也是最大的坑:它依赖一个原生的SQLite.Interop.dll,而这个 dll 分 x86 和 x64 两个版本。很多开发包解压后bin目录里只有一份,或者两份混在一起,导致运行时直接抛Unable to load DLL 'SQLite.Interop.dll'。
先做一件事:在解压目录里搜索SQLite.Interop.dll,看清楚它出现在哪些子目录下。常见结构是x86/SQLite.Interop.dll和x64/SQLite.Interop.dll各一份,由System.Data.SQLite.dll在运行时按进程位数自动加载。如果你的项目是 AnyCPU 且没做处理,在 64 位系统上跑 32 位进程就会找不到对应文件。
2.2 用命令行确认开发包能不能编译,别依赖 IDE
在动手改代码前,先用dotnet或msbuild在命令行编译一次,把环境问题和代码问题分开。假设开发包根目录有一个SQLiteDemo.csproj:
# 进入开发包目录,先还原依赖 dotnet restore SQLiteDemo.csproj # 指定平台编译,避免 AnyCPU 带来的 Interop 加载歧义 dotnet build SQLiteDemo.csproj -c Debug -p:Platform=x64这两条命令的含义:restore负责把 NuGet 包拉下来,如果开发包用的是packages.config老格式,这一步可能不生效,需要改用nuget restore。build时显式指定Platform=x64,是为了让输出目录里带上正确的SQLite.Interop.dll。如果你不确定开发包是哪种包管理格式,看根目录有没有packages.config:有就是老格式,没有且.csproj里有<PackageReference>就是新格式。
编译成功后,去bin/Debug下确认三样东西:System.Data.SQLite.dll、SQLite.Interop.dll、以及实例用的.db文件。少任何一样,运行都会出问题。这一步看起来笨,但能帮你把「环境问题」和「代码问题」彻底分开,后面调试会省很多时间。
2.3 实例源码里最值得先读的两个文件
一个典型的 C# SQLite 实例源码包,文件很多,但真正决定你能不能跑通的往往只有两个:数据库连接封装类和主入口。连接封装类通常叫SQLiteHelper.cs或DbHelper.cs,里面会有连接字符串、打开/关闭连接、执行 SQL 的方法。主入口就是Program.cs或某个窗体的Load事件。
先读连接字符串。常见写法是:
string connStr = "Data Source=test.db;Version=3;";这里Data Source是数据库文件路径,可以是相对路径也可以是绝对路径。相对路径是相对于程序的工作目录,不是相对于 exe 所在目录,这一点很多人搞混。Version=3表示 SQLite 3 格式。如果实例里还写了Password=xxx,那说明这个包用了加密版 SQLite,普通System.Data.SQLite打不开,需要对应的加密版本,这是后面避坑章节要讲的一个大坑。
读主入口时,重点看它有没有在启动时自动建表、插入测试数据。如果有,先别改逻辑,直接跑,看能不能在bin目录下生成.db文件。能生成,说明开发包基本可用;不能生成,再回头查 Interop 和路径问题。
3. 跑通第一个实例:从建库到增删改查的最小闭环
3.1 用一段最小代码验证 SQLite 连接是否真的可用
不要一上来就跑完整实例,先写一个最小验证程序。新建一个控制台项目,引用开发包里的System.Data.SQLite.dll,然后写下面这段:
using System; using System.Data.SQLite; class Program { static void Main() { // 数据库文件放在当前工作目录,避免路径歧义 string dbPath = "minimal_test.db"; string connStr = $"Data Source={dbPath};Version=3;"; // 如果文件不存在,CreateFile 会新建一个空库 if (!System.IO.File.Exists(dbPath)) { SQLiteConnection.CreateFile(dbPath); } using (var conn = new SQLiteConnection(connStr)) { conn.Open(); // 建一张最小表,只放两个字段 string createSql = "CREATE TABLE IF NOT EXISTS Demo (Id INTEGER PRIMARY KEY, Name TEXT)"; using (var cmd = new SQLiteCommand(createSql, conn)) { cmd.ExecuteNonQuery(); } // 插入一条数据,用参数化避免拼接 string insertSql = "INSERT INTO Demo (Name) VALUES (@name)"; using (var cmd = new SQLiteCommand(insertSql, conn)) { cmd.Parameters.AddWithValue("@name", "first_row"); cmd.ExecuteNonQuery(); } // 查询并打印 string selectSql = "SELECT Id, Name FROM Demo"; using (var cmd = new SQLiteCommand(selectSql, conn)) using (var reader = cmd.ExecuteReader()) { while (reader.Read()) { Console.WriteLine($"Id={reader.GetInt32(0)}, Name={reader.GetString(1)}"); } } } Console.WriteLine("done"); } }这段代码的逻辑说明:SQLiteConnection.CreateFile只在文件不存在时创建空数据库文件,不会覆盖已有数据。using保证连接和命令对象及时释放,避免文件被锁。插入时用@name参数化,而不是字符串拼接,这是防止 SQL 注入和特殊字符出错的基本习惯。查询用ExecuteReader逐行读,GetInt32(0)和GetString(1)按列序号取值,比列名取值稍快,但列名更直观,实例源码里两种都有。
参数说明:Data Source如果写相对路径,程序的工作目录决定文件位置。在 Visual Studio 里调试时工作目录默认是bin/Debug,在命令行运行时是当前终端目录。如果你发现数据库文件「找不到」,先打印Environment.CurrentDirectory确认工作目录。
3.2 把实例源码里的增删改查封装看懂并改造成自己的
跑通最小验证后,再回头看开发包里的SQLiteHelper。一个常见的封装长这样:
public static int ExecuteNonQuery(string sql, params SQLiteParameter[] parameters) { using (var conn = new SQLiteConnection(connStr)) { conn.Open(); using (var cmd = new SQLiteCommand(sql, conn)) { if (parameters != null && parameters.Length > 0) { cmd.Parameters.AddRange(parameters); } return cmd.ExecuteNonQuery(); } } }这个封装的优点是简单直接,缺点是每次调用都新开一个连接。SQLite 的连接开销很小,这种写法在单线程上位机里够用。但如果你在循环里调用几千次,就会明显变慢。改进方式是传入一个已打开的SQLiteConnection,或者用事务包起来。
改造时我一般会加一个ExecuteScalar和一个泛型查询方法,方便取单个值和映射到对象。但不要一上来就上 EF Core 或 Dapper,先把原生 ADO.NET 的封装吃透,后面换 ORM 只是换写法,底层还是这些。
3.3 用事务把批量插入从「秒级」压到「毫秒级」
实例源码里如果有批量插入,大概率是逐条ExecuteNonQuery。SQLite 默认每条语句自动提交,每次提交都要写磁盘,几千条下来会非常慢。正确做法是用事务:
using (var conn = new SQLiteConnection(connStr)) { conn.Open(); using (var trans = conn.BeginTransaction()) { using (var cmd = new SQLiteCommand("INSERT INTO Demo (Name) VALUES (@name)", conn)) { cmd.Parameters.Add("@name", System.Data.DbType.String); for (int i = 0; i < 5000; i++) { cmd.Parameters["@name"].Value = "row_" + i; cmd.ExecuteNonQuery(); } } trans.Commit(); } }关键点:BeginTransaction之后所有插入都在同一个事务里,最后一次性Commit。参数对象只创建一次,循环里只改Value,避免反复创建参数。这个改动通常能把 5000 条插入从几秒降到几十毫秒。注意事务不要开太久,如果中间要处理 UI 消息,记得分批提交,否则会阻塞界面。
4. 参数与配置:连接字符串、并发和文件锁的边界
4.1 连接字符串里真正需要调的三个参数
System.Data.SQLite的连接字符串参数很多,但日常真正需要动的就三个:
| 参数 | 作用 | 建议值 |
|---|---|---|
Data Source | 数据库文件路径 | 用绝对路径或明确的工作目录相对路径 |
Version | SQLite 格式版本 | 固定3 |
Pooling | 是否启用连接池 | 单线程上位机可设False,多线程设True |
Journal Mode | 日志模式 | 读多写少用WAL,单写入用默认Delete |
Pooling=True时,连接关闭后不会真正释放,而是放回池里,下次打开更快。但在 SQLite 里,连接池和文件锁有时会打架,尤其是你手动删.db文件时会提示被占用。如果你在做单机上位机,Pooling=False更省心。
Journal Mode=WAL是 SQLite 的预写日志模式,允许多个读和一个写同时进行,适合读多写少的场景。设置方式是在连接字符串里加Journal Mode=WAL;,或者在打开连接后执行PRAGMA journal_mode=WAL;。WAL 模式会额外生成-wal和-shm文件,拷贝数据库时要一起拷,否则可能丢数据。
4.2 多线程上位机里 SQLite 的并发边界
C# 上位机经常有多个线程:串口接收线程、数据处理线程、UI 线程。如果多个线程同时写同一个 SQLite 数据库,会抛database is locked。SQLite 本身支持多读单写,但写锁是排他的。
常见做法是:所有写操作走一个专用队列或锁。简单点用lock:
private static readonly object dbLock = new object(); public static void SafeInsert(string name) { lock (dbLock) { // 这里调用 ExecuteNonQuery } }更优雅的做法是用BlockingCollection做一个写入队列,后台单线程消费。这样 UI 线程不会因为等锁而卡顿。注意lock只能保证同一进程内互斥,如果你有多个进程同时写同一个.db文件,SQLite 的文件锁会介入,但性能会下降,且容易超时。多进程场景建议每个进程写自己的库,或者改用客户端-服务端数据库。
4.3 数据库文件被占用、删不掉、拷不走的排查顺序
现象:程序关了,但.db文件删不掉,提示「文件正在被另一个程序使用」。原因通常是连接没有正确释放,或者连接池还持有句柄。排查顺序:
- 确认所有
SQLiteConnection都用了using或显式Dispose。 - 如果用了
Pooling=True,调用SQLiteConnection.ClearAllPools()清池。 - 检查是否有
SQLiteDataReader没关闭,reader 不关会一直占着连接。 - 如果用了 WAL 模式,
.db-wal和.db-shm文件也要一起处理。
我一般会在程序退出时加一句SQLiteConnection.ClearAllPools(),这个习惯帮我省了很多「文件被占用」的玄学问题。
5. 避坑与常见问题:Interop、加密库和路径的三个血泪教训
5.1 现象:Unable to load DLL 'SQLite.Interop.dll'
原因:System.Data.SQLite是托管 dll,它需要同目录下对应位数的原生SQLite.Interop.dll。AnyCPU 项目在 64 位系统上默认跑 64 位进程,但如果输出目录只有 x86 的 Interop,就会加载失败。
解决:在.csproj里显式指定平台,或者用 NuGet 包System.Data.SQLite.Core,它会把 x86 和 x64 的 Interop 都放到runtimes目录下自动加载。如果你用的是老开发包,手动把x64/SQLite.Interop.dll拷到输出目录,并确保项目平台设为 x64。
5.2 现象:打开数据库提示「file is encrypted or is not a database」
原因:开发包里的.db文件是用加密版 SQLite 创建的,比如 SQLCipher 或System.Data.SQLite的加密版。普通System.Data.SQLite.dll没有解密能力,打开就报这个错。
解决:确认开发包是否附带加密版 dll。如果是 SQLCipher,需要引用SQLitePCLRaw.bundle_e_sqlcipher并设置Password参数。如果拿不到密码或加密库,这个.db文件基本打不开,只能找原始开发包或重新建库。这也是为什么我拿到开发包第一件事是看它有没有Password=和加密 dll。
5.3 现象:程序在 IDE 里能跑,双击 exe 就找不到数据库
原因:IDE 调试时工作目录是bin/Debug,双击 exe 时工作目录是 exe 所在目录,两者可能不同。如果连接字符串用的是相对路径,数据库文件位置就会变。
解决:用AppDomain.CurrentDomain.BaseDirectory拼绝对路径:
string dbPath = System.IO.Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "data.db");这样无论从哪里启动,数据库文件都在 exe 旁边。如果希望数据库放在用户目录,用Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData)。
5.4 现象:插入中文变成乱码或问号
原因:SQLite 默认用 UTF-8 存储文本,C# 的string是 UTF-16。正常通过参数化插入不会乱码。乱码通常出现在用字符串拼接 SQL 且源文件编码不是 UTF-8 时。
解决:统一用参数化,不要拼接。如果必须拼接,确保.cs文件保存为 UTF-8 with BOM,并在连接字符串里不需要额外设置编码。另外,用 DB Browser for SQLite 打开.db文件查看时,如果显示乱码,先确认查看工具的编码设置,不一定是数据本身的问题。
5.5 现象:SQLiteException: database is locked
原因:有另一个连接或进程正在写,或者上一个事务没提交/回滚。WAL 模式下读不会阻塞写,但写会阻塞写。
解决:检查是否有未释放的SQLiteCommand或SQLiteDataReader。给写操作加锁或队列。设置BusyTimeout让 SQLite 在锁冲突时等待而不是立刻报错:
string connStr = "Data Source=data.db;Version=3;BusyTimeout=5000;";BusyTimeout=5000表示锁冲突时最多等 5 秒。这个参数在上位机频繁写入时很有用,但不要设太大,否则 UI 会卡。
6. 进阶:把开发包改造成可复用的 SQLite 访问层
6.1 用 Dapper 简化查询,但保留原生连接的控制权
当你把原生 ADO.NET 跑通后,可以引入 Dapper 来减少样板代码。Dapper 不是 ORM,它只是IDbConnection的扩展方法,底层还是SQLiteConnection。这样你既能用Query<T>自动映射,又能控制连接和事务。
using Dapper; using System.Data.SQLite; public class DemoRow { public int Id { get; set; } public string Name { get; set; } } public static List<DemoRow> GetAll() { using (var conn = new SQLiteConnection("Data Source=data.db;Version=3;")) { return conn.Query<DemoRow>("SELECT Id, Name FROM Demo").AsList(); } }Query<DemoRow>会把列名映射到属性名,大小写不敏感。AsList()立即执行并返回List<T>。注意 Dapper 不负责建表,表结构还是自己维护。如果你的开发包实例源码里全是手写 reader,改成 Dapper 后代码量能少一半,但调试时要注意 SQL 错误信息还是来自 SQLite 本身。
6.2 用 DB Browser for SQLite 验证数据,别只信代码
写完插入逻辑后,不要只看控制台输出。用 DB Browser for SQLite 打开.db文件,直接看表结构和数据。这个工具能帮你确认:表是否真的建了、字段类型对不对、数据有没有写进去、WAL 文件是否正常合并。
如果 DB Browser 打不开,提示数据库被锁,先关掉你的 C# 程序,再打开。如果还是打不开,检查.db文件是否 0 字节,或者是否被加密。这个工具是 SQLite 开发里最值得装的辅助工具,比在代码里Console.WriteLine高效得多。
6.3 一个我坚持了多年的习惯:先备份 .db 再改代码
SQLite 开发最让人后悔的事,就是改代码时把测试数据搞丢了,或者一个DELETE没加WHERE把整张表清空。我的习惯是:每次改涉及写操作的代码前,先把.db文件复制一份,命名带日期。如果用了 WAL 模式,连.db-wal和.db-shm一起复制。
另外,在测试环境里不要用生产数据库文件。开发包里的实例数据库可以随便折腾,但一旦接入真实上位机数据,先建一个空库跑通流程,再切换。这个习惯看起来笨,但帮我省过好几次「数据没了」的麻烦。
希望帮到你。
本文还有配套的精品资源,点击获取