news 2026/9/26 4:42:38

C# SQLite开发包实战:从跑通实例到封装避坑

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
C# SQLite开发包实战:从跑通实例到封装避坑

简介:这份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.SQLiteSystem.Data.SQLite.dll+SQLite.Interop.dll官方老牌,功能全,需处理 x86/x64传统 WinForm/WPF 上位机
Microsoft.Data.SqliteNuGet 包Microsoft.Data.Sqlite微软维护,跨平台,依赖少.NET Core/.NET 6+ 新项目
SQLitePCLRaw + EF CoreMicrosoft.EntityFrameworkCore.SqliteORM 方式,写实体类即可需要 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数据库文件路径用绝对路径或明确的工作目录相对路径
VersionSQLite 格式版本固定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文件删不掉,提示「文件正在被另一个程序使用」。原因通常是连接没有正确释放,或者连接池还持有句柄。排查顺序:

  1. 确认所有SQLiteConnection都用了using或显式Dispose。
  2. 如果用了Pooling=True,调用SQLiteConnection.ClearAllPools()清池。
  3. 检查是否有SQLiteDataReader没关闭,reader 不关会一直占着连接。
  4. 如果用了 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一起复制。

另外,在测试环境里不要用生产数据库文件。开发包里的实例数据库可以随便折腾,但一旦接入真实上位机数据,先建一个空库跑通流程,再切换。这个习惯看起来笨,但帮我省过好几次「数据没了」的麻烦。

希望帮到你。

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

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

Spring Boot+Vue水果电商系统实战:从需求到部署全流程解析

做这个项目之前&#xff0c;我对攀枝花的印象基本停留在“钢铁之都”这个词上。真正去了一趟果农的合作社才发现&#xff0c;金沙江河谷的干热气候把这里变成了水果产区——晚熟芒果能一直卖到十月&#xff0c;早春枇杷错峰上市&#xff0c;价格比海南货高出一截。这套基于spri…

作者头像 李华
网站建设 2026/9/26 4:41:10

移动医疗零漫游无线方案:病房信号覆盖与漫游问题解析

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

作者头像 李华
网站建设 2026/9/26 4:41:01

StatefulSet必填字段serviceName与有状态应用初始化解析

你一定见过这种场面&#xff1a;照着网上的 MySQL 主从 StatefulSet 抄了一份 YAML&#xff0c;自认为看懂了所有字段&#xff0c;顺手把那个"看着就多余"的serviceName删掉&#xff0c;然后kubectl apply直接甩给你一句&#xff1a;ValidationError(StatefulSet.spe…

作者头像 李华
网站建设 2026/9/26 4:40:32

SpringBoot+Vue旅游管理系统开发实战:从数据库设计到前后端联调

做旅游类管理系统&#xff0c;我前后折腾过不下三次&#xff0c;从最早的 JSP Servlet 古董组合&#xff0c;到后来改用 SpringBoot Vue 这套前后端分离的方案&#xff0c;算是把整个流程摸了一遍。今天就拿这个“基于 SpringBoot Vue 的旅游管理系统”当样板&#xff0c;把…

作者头像 李华
网站建设 2026/9/26 4:40:28

Linux系统安装与程序管理全攻略:从选型到systemd服务排查

1. 安装前的思路&#xff1a;别再纠结装哪个发行版&#xff0c;先想清楚拿它干嘛最近后台收到不少关于Linux安装和程序管理的私信&#xff0c;大部分问题集中在“装到一半蓝屏”“软件装不上”“依赖冲突搞死人”这几类。说实话&#xff0c;这些坑我早期都踩过一遍&#xff0c;…

作者头像 李华
网站建设 2026/9/26 4:39:55

Kubernetes DaemonSet详解:节点守护、调度策略与生产实践

1. 为什么需要DaemonSet&#xff1a;一批“长在节点上”的守护进程先从一个最朴素的问题说起&#xff1a;Kubernetes里已经有Deployment、StatefulSet、Job这些工作负载&#xff0c;它们能把Pod调度到集群的各个节点上&#xff0c;为什么还要单独搞一个DaemonSet控制器&#xf…

作者头像 李华