简介:面向C#开发者的一份SQLite数据库开发与学习资料,适合刚接触嵌入式数据库的初学者,也适合需要在桌面项目中快速集成轻量级存储方案的中级开发者。压缩包共85个文件,约19.17MB,以52个dll动态库为主,配合8个cs源码、6个exe可执行程序及config、resx、sln、csproj等工程配置与资源文件,另附一个db数据库样本,既有可直接运行的示例工程,也包含SQLiteStudio等可视化工具,便于对照理解数据库结构与调试过程。资源围绕SQLite轻量、独立、无需安装、跨平台、多语言接口及事务锁机制等特点展开,读者可从中获得完整的示例项目结构、可复用的数据访问代码和工具脚本,快速搭建本地数据存储环境并验证增删改查逻辑。目前已有959人学习下载,适合作为C#结合SQLite入门的实践参考。
1. 从一份压缩包说起:C# 里把 SQLite 用顺手到底要几步
很多人第一次在 C# 项目里接 SQLite,都是被一个“本地数据库”需求推着走的:桌面端要离线存数据、上位机要缓存采集记录、小工具不想装数据库服务。真动手时才发现,光一个System.Data.SQLite就够折腾——NuGet 装哪个包、x86 和 x64 的 native dll 怎么放、连接字符串怎么写、事务为什么没生效。这份 C# SQLite 开发包及实例源码,本质上就是把这些零碎问题打包成一套能直接跑的东西:里面有可引用的驱动组件,也有配套的实例源码,覆盖建库建表、增删改查、参数化查询、事务提交这些日常操作。适合刚接触 C# 数据持久化的新手照着跑,也适合想快速搭个本地存储骨架的老手拿来改。下面按“能干什么 → 怎么跑 → 怎么改”的顺序拆。
2. 开发包结构拆解:驱动、实例与目录组织
2.1 压缩包里都有什么
拿到一个 C# SQLite 开发包,先别急着双击 sln。我一般先看目录,判断它是“纯驱动包”还是“驱动 + 示例工程”的组合。这类包常见的组织方式大致如下(不同版本细节会有出入,但骨架类似):
CSharpSQLiteDemo/ ├── lib/ # 驱动与依赖 │ ├── System.Data.SQLite.dll │ ├── SQLite.Interop.dll # native 互操作库,分 x86/x64 │ └── ... ├── src/ │ ├── SQLiteDemo.Core/ # 数据访问封装层 │ │ ├── DbHelper.cs │ │ └── Models/ │ └── SQLiteDemo.App/ # 示例入口(控制台或 WinForm) │ └── Program.cs ├── sql/ │ └── init.sql # 建表脚本 └── README.md看到SQLite.Interop.dll就要警觉:这是托管层调 native 层的桥,位数必须和宿主进程一致。很多人跑不起来不是代码错,而是这个 dll 放错目录或位数不匹配。lib放驱动,src放实例,sql放初始化脚本,这种分层说明作者是想让你“先跑示例、再抽封装”。
2.2 技术栈与选型逻辑
这套东西的技术栈很朴素:C# + ADO.NET + SQLite。没有 ORM,没有 EF Core,走的是最原始的SQLiteConnection/SQLiteCommand/SQLiteDataReader路线。为什么实例源码常这么写?因为教学和二次开发都友好——你能清楚看到每条 SQL 怎么发出去、参数怎么绑、结果怎么读,出了问题能定位到具体一行,而不是在 ORM 的表达式树里猜。
| 层次 | 组件 | 作用 |
|---|---|---|
| 驱动层 | System.Data.SQLite | 提供 ADO.NET 接口,托管代码调用 |
| 互操作层 | SQLite.Interop.dll | 桥接 native SQLite 引擎 |
| 封装层 | DbHelper 之类 | 统一连接、执行、事务 |
| 应用层 | 示例工程 | 演示增删改查调用方式 |
选 ADO.NET 直连而不是 EF Core,还有个现实原因:SQLite 在桌面和小工具场景里,往往只需要几十行数据访问代码,引 EF Core 反而带来迁移、上下文生命周期这些额外概念。实例源码用直连,读者抄过去就能用,改起来也直观。
2.3 连接字符串与数据库文件位置
SQLite 是文件型数据库,连接字符串的核心就是指向那个.db文件。实例里常见两种写法:
// 写法一:相对路径,数据库文件落在程序运行目录 string connStr = "Data Source=app.db;Version=3;"; // 写法二:绝对路径 + 常用参数 string connStr = @"Data Source=D:\data\app.db;Version=3;Pooling=True;FailIfMissing=False;";Data Source是文件路径,Version=3表示 SQLite 3 语法,Pooling=True开启连接池减少反复打开文件的开销,FailIfMissing=False表示文件不存在时自动创建。新手最容易踩的是相对路径——调试时工作目录是bin\Debug,发布后又变成安装目录,数据库文件“凭空消失”多半是这里。我的习惯是启动时把数据库路径拼成基于AppDomain.CurrentDomain.BaseDirectory的绝对路径,避免玄学。
3. 核心代码逐段讲:从建表到事务提交
3.1 打开连接与建表
先看最基础的一段:打开连接、执行建表脚本。实例源码里通常封装成一个ExecuteNonQuery方法。
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(); // 返回受影响行数 } } }逻辑很直白:using保证连接和命令对象释放,Open()真正打开文件,ExecuteNonQuery用于 INSERT/UPDATE/DELETE/DDL 这类不返回结果集的语句。参数用params数组传入,调用方可以写ExecuteNonQuery(sql, new SQLiteParameter("@name", "张三"))。这里的关键点是永远走参数化,别用字符串拼接——SQLite 虽然本地,但拼接一样有注入和转义问题,名字里带个单引号就能让语句崩掉。
建表调用示例:
string createSql = @" CREATE TABLE IF NOT EXISTS DeviceLog ( Id INTEGER PRIMARY KEY AUTOINCREMENT, DeviceNo TEXT NOT NULL, Temp REAL, Created TEXT NOT NULL );"; DbHelper.ExecuteNonQuery(createSql);INTEGER PRIMARY KEY AUTOINCREMENT是 SQLite 的自增主键写法,IF NOT EXISTS保证重复执行不报错,适合放在程序启动时做幂等初始化。Created用 TEXT 存 ISO 时间字符串,是 SQLite 没有原生日期类型时的常见做法。
3.2 参数化查询与结果读取
查询要返回结果集,用ExecuteReader逐行读。实例里一般会封装成返回DataTable或泛型列表。
public static List<DeviceLog> QueryLogs(string deviceNo, DateTime from) { var list = new List<DeviceLog>(); string sql = "SELECT Id, DeviceNo, Temp, Created FROM DeviceLog " + "WHERE DeviceNo = @no AND Created >= @from ORDER BY Created DESC"; using (var conn = new SQLiteConnection(ConnStr)) { conn.Open(); using (var cmd = new SQLiteCommand(sql, conn)) { cmd.Parameters.AddWithValue("@no", deviceNo); cmd.Parameters.AddWithValue("@from", from.ToString("s")); // ISO 8601 using (var reader = cmd.ExecuteReader()) { while (reader.Read()) { list.Add(new DeviceLog { Id = reader.GetInt64(0), DeviceNo = reader.GetString(1), Temp = reader.IsDBNull(2) ? 0 : reader.GetDouble(2), Created = reader.GetString(3) }); } } } } return list; }逐段说明:AddWithValue省事但要注意类型推断,@from传字符串是因为列是 TEXT,用"s"格式(yyyy-MM-ddTHH:mm:ss)保证字典序和时间序一致,这样>=比较才正确。reader.GetInt64(0)按列序号取值,比列名快但可读性差,实例里两种都有,改的时候建议用列名。IsDBNull判断必须加,Temp允许为空时直接GetDouble会抛异常。ORDER BY Created DESC配合索引能避免全表扫描,数据量上万后差别明显。
3.3 事务:批量写入的正确姿势
单条写入慢,批量插入必须上事务。实例里通常有一段“循环插入 + 事务包裹”的演示。
public static void BatchInsert(List<DeviceLog> logs) { using (var conn = new SQLiteConnection(ConnStr)) { conn.Open(); using (var tran = conn.BeginTransaction()) // 开启事务 { using (var cmd = new SQLiteCommand(conn)) { cmd.Transaction = tran; // 命令绑定事务,别漏 cmd.CommandText = "INSERT INTO DeviceLog(DeviceNo,Temp,Created) " + "VALUES(@no,@temp,@created)"; var pNo = cmd.Parameters.Add("@no", DbType.String); var pTemp = cmd.Parameters.Add("@temp", DbType.Double); var pCreated = cmd.Parameters.Add("@created", DbType.String); foreach (var log in logs) { pNo.Value = log.DeviceNo; pTemp.Value = log.Temp; pCreated.Value = log.Created; cmd.ExecuteNonQuery(); } } tran.Commit(); // 全部成功才提交 } } }这段是性能关键:不开事务时每条 INSERT 都会触发一次磁盘同步,几千条能跑出几十秒;包上事务后写入合并,速度差一个数量级。两个细节容易翻车——cmd.Transaction = tran必须设,否则命令不在事务上下文里;参数对象在循环外创建、循环内只改Value,避免反复解析 SQL。异常时using会触发回滚,不用手写Rollback,但如果你要捕获异常继续跑,就得自己控制提交时机。
3.4 封装层的接口设计
把上面几个方法收进一个DbHelper静态类,接口大致是:
| 方法 | 用途 | 返回 |
|---|---|---|
| ExecuteNonQuery | 增删改、DDL | 受影响行数 |
| ExecuteScalar | 取单值(COUNT、MAX) | object |
| QueryLogs / Query | 查询列表 | List |
| BatchInsert | 批量写入 | void |
封装层不追求大而全,够用就行。二次开发时你可以把它换成实例化的 Repository,或者加一层缓存,但接口形状保持稳定,上层调用就不用动。
4. 跑起来:环境准备与首次运行排查
4.1 环境与依赖确认
跑这套实例前,先确认三件事:.NET 运行时版本、项目目标框架、驱动位数。实例工程常见是 .NET Framework 4.x 或 .NET 6/8 控制台。用 Visual Studio 打开 sln 后,先看项目属性里的目标框架,再确认lib下的SQLite.Interop.dll有没有 x86 和 x64 两个子目录。如果只有一份,说明它绑定了特定平台,项目属性里的“平台目标”要跟着设成对应值,不能留AnyCPU还勾“首选 32 位”。
# 命令行方式还原并运行(.NET 6+ 项目) dotnet restore dotnet build -c Debug dotnet run --project src/SQLiteDemo.App如果是 .NET Framework 工程,就用msbuild或直接在 IDE 里 F5。首次运行前把sql/init.sql里的建表语句确认一遍,多数实例会在Program.Main里自动执行初始化,不需要你手动建库。
4.2 首次运行的正确顺序
我一般按这个顺序走,能少走弯路:先跑示例工程确认驱动可用,再改连接字符串指向自己的库,最后才动封装层。示例工程跑通的标准是控制台打印出插入条数和查询结果,或者 WinForm 界面上能看到数据。如果卡在第一步,问题基本在驱动加载,不在业务代码。跑通后再把DbHelper抽到自己项目里,改ConnStr为你的路径,建表脚本按你的字段改,整个迁移半小时内能完成。
4.3 验证数据是否真的落盘
别只看程序不报错就以为成了。打开数据库文件所在目录,确认app.db文件存在且大小在增长;用任意 SQLite 客户端工具打开,执行SELECT COUNT(*) FROM DeviceLog;看行数对不对。这一步能抓出“连接到了内存库”或“写到了另一个目录”的问题——SQLite 的Data Source=:memory:是内存库,关连接就没了,有人抄配置时没注意就中招。
5. 避坑与常见问题:那些让程序跑不起来的细节
5.1 报“无法加载 SQLite.Interop.dll”
现象:运行时报Unable to load DLL 'SQLite.Interop.dll'或BadImageFormatException。原因:native 库位数和进程不匹配,或没被复制到输出目录。解决:确认项目平台目标(x86/x64)与SQLite.Interop.dll所在子目录一致;在驱动包的属性里把 Interop 的“复制到输出目录”设为“始终复制”;AnyCPU项目勾选“首选 32 位”时要确保有 x86 版本。
5.2 数据库文件“找不到”或每次都是空库
现象:程序能跑,但查不到之前写的数据。原因:连接字符串用了相对路径,调试和发布时工作目录不同,实际打开了不同文件。解决:统一用Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "app.db")拼绝对路径,启动时打印一次实际路径,肉眼确认。
5.3 批量插入慢到怀疑人生
现象:循环插一万条要几十秒。原因:没包事务,每条语句独立提交并同步磁盘。解决:按 3.3 的方式用BeginTransaction包裹,命令绑定事务;数据量特别大时再考虑分批提交,比如每 5000 条 Commit 一次,兼顾速度和回滚粒度。
5.4 并发写入报“database is locked”
现象:多线程或多进程同时写,抛SQLiteException: database is locked。原因:SQLite 默认写锁是库级的,同一时刻只允许一个写者。解决:写操作串行化,用锁或单写线程;连接字符串加BusyTimeout=5000让驱动自动重试等待;读多写少场景可开 WAL 模式(PRAGMA journal_mode=WAL;)提升并发读能力。
5.5 时间比较结果不对
现象:按时间范围查询,明明有数据却查不出来。原因:时间存成了本地格式(如2024/1/5 9:00:00),字典序和时间序不一致。解决:统一存 ISO 8601 字符串(yyyy-MM-ddTHH:mm:ss),查询参数也用同样格式;或者存 Unix 时间戳整数,比较最稳。
6. 二次开发:把示例改造成自己的数据层
拿到实例源码后,真正有价值的是把它变成你项目里的数据访问层,而不是停在“能跑”。我的改造路径通常分三步。第一步,把DbHelper从静态类改成可注入的实例类,构造函数接收连接字符串,这样单元测试能指向临时库,生产指向正式库,不用改全局变量。第二步,给每张表建对应的 Repository,把 SQL 收进各自类里,DeviceLogRepository只管DeviceLog,避免一个 Helper 里堆几十条 SQL 变成黑匣子。第三步,加一层轻量映射,查询结果从reader手动映射改成泛型方法,减少重复代码。
public class DeviceLogRepository { private readonly string _connStr; public DeviceLogRepository(string connStr) => _connStr = connStr; public IReadOnlyList<DeviceLog> GetRecent(string deviceNo, int take = 100) { // 复用参数化查询,take 用 LIMIT 控制返回量 string sql = "SELECT Id,DeviceNo,Temp,Created FROM DeviceLog " + "WHERE DeviceNo=@no ORDER BY Created DESC LIMIT @take"; // ... 打开连接、绑定参数、读取映射 } }验证改造是否成功,别只靠“跑一遍没报错”。我会写一个小的冒烟测试:建临时库 → 插 100 条 → 查回条数 → 断言相等 → 删库。这套流程每次改数据层都跑一遍,比手工点界面可靠。还有个习惯值得养成:所有 DDL 变更都走版本化脚本,比如sql/v1_init.sql、sql/v2_add_index.sql,启动时按版本号顺序执行并记录到SchemaVersion表,避免不同机器上库结构不一致这种后悔药都买不到的问题。
最后说个血泪教训:早期我图省事,把连接字符串硬编码在 Helper 里,结果测试环境和现场环境来回改,改漏一次就把现场数据写进了测试库。从那以后我每次新建数据层,都强制把连接配置外置到配置文件或环境变量,并且启动时打印实际连接的库路径。希望帮到你,少踩几个我已经踩过的坑。
本文还有配套的精品资源,点击获取