简介:面向C#开发者的SQLite数据库开发整合包,内含可直接引用的System.Data.SQLite动态库与配套实例工程,解决在.NET环境下集成轻量级数据库时的环境配置与代码参考问题。资源共85个文件,以dll运行库、cs源码、exe工具为主,辅以config配置文件、resx资源文件及解决方案工程文件,既支持原生调用也支持图形化管理;压缩包19.17MB,便于快速下载部署。目前已有954人学习使用。实例源码涵盖数据库连接、建表、增删改查等典型操作,配合SQLiteStudio可视化工具,可帮助初学者理解SQLite的轻量、独立、跨平台等特性,也能让有经验的开发者直接复用代码与配置,减少踩坑成本。整体目录结构清晰,适合课堂教学、个人项目集成以及快速原型验证等场景。
1. C# SQLite开发包及实例源码:为什么桌面离线存储绕不开它
做上位机、桌面工具或者单机版管理系统的人,多半会遇到同一个需求:软件要能断网运行,数据量不大但结构固定,又不能要求用户去装数据库服务。这种场景下,在C#项目里集成SQLite是最顺手的方案。这份C# SQLite开发包及实例源码把驱动、连接封装和带增删改查的Demo工程放在一起,下载后不用从零配环境,改一个连接字符串就能跑通。它适合正在写库存管理、数据采集上位机,或想系统学习ADO.NET与SQLite配合方式的开发者,也适合直接把源码当模板改业务逻辑。
2. 把SQLite集成进C#项目:包选择、初始化与第一个表
拿到一包源码,第一步不是读代码,而是确认它用的是哪个驱动,连接字符串怎么写的。这一步错了,后面所有业务代码都是白跑。这套实例源码基于System.Data.SQLite,思路非常明确:用ADO.NET风格操作SQLite,迁移成本低,老工程师接手也快。
2.1 System.Data.SQLite还是Microsoft.Data.Sqlite:先搞清楚驱动差异
C#里连接SQLite的主流驱动有两个:System.Data.SQLite和Microsoft.Data.Sqlite。两者的表面用法差不多,都实现了ADO.NET接口,都能用SQLiteConnection、SQLiteCommand,但底层差别很大,直接影响你部署和踩坑的方式。
System.Data.SQLite是对原生SQLite库的封装,里面带C++编译的SQLite.Interop.dll,因此它对PRAGMA、扩展加载、AUTOINCREMENT这类SQLite特性的支持最完整。实例源码里大量用到“PRAGMA journal_mode=WAL”这类原生指令,选这个驱动是合理的,因为它几乎不过滤SQLite的原生能力。
Microsoft.Data.Sqlite则是纯托管的轻量实现,跨平台更好,部署干净,不需要带原生DLL,但它在类型映射和部分SQLite功能的支持上做了取舍。比如某些老版本的扩展函数、加密插件就可能用不了。如果你以后要改成Linux下运行,再考虑迁移到它。迁移时SQL语句可以原样复用,但连接对象和命令对象的命名空间、部分参数类型要换一层。
驱动选型建议就一句话:Windows桌面工具、WinForms/WPF项目、需要用到WAL和PRAGMA的,优先System.Data.SQLite;跨平台或纯容器部署,再考虑Microsoft.Data.Sqlite。
2.2 用NuGet完成集成并初始化数据库文件
在Visual Studio的NuGet包管理控制台里执行下面这条命令,就能把托管DLL和原生DLL按平台自动放进输出目录:
Install-Package System.Data.SQLite.Core执行完成后,项目的bin目录下会出现x86、x64子目录,里面是SQLite.Interop.dll。这个文件很关键,发布时如果漏了它,程序在你本机能跑、换台机器就报“无法加载DLL”,后面避坑章节会专门讲。
初始化数据库文件的代码在实例源码里一般长这样:
using System.Data.SQLite; string dbPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "appdata.db"); string connStr = $"Data Source={dbPath};Version=3;Foreign Keys=True;Journal Mode=WAL;Busy Timeout=3000;"; using var conn = new SQLiteConnection(connStr); conn.Open(); string createSql = @" CREATE TABLE IF NOT EXISTS inventory ( id INTEGER PRIMARY KEY AUTOINCREMENT, item_code TEXT NOT NULL, item_name TEXT, quantity INTEGER DEFAULT 0, updated_at TEXT DEFAULT (datetime('now','localtime')) );"; using var cmd = new SQLiteCommand(createSql, conn); cmd.ExecuteNonQuery();这段代码的核心逻辑是:Open连接时,如果appdata.db文件不存在,SQLite会自动创建数据库文件;CREATE TABLE IF NOT EXISTS保证重复启动不会报“表已存在”的错误。
连接字符串里的参数值得逐个看:
| 参数 | 取值 | 作用 |
|---|---|---|
| Data Source | 数据库文件路径 | 使用Path.Combine拼到程序目录,避免相对路径漂移 |
| Version | 3 | 指定SQLite版本格式,固定写3 |
| Foreign Keys | True | 开启外键约束,SQLite默认是关闭的 |
| Journal Mode | WAL | 使用预写日志模式,提升读写并发能力 |
| Busy Timeout | 3000 | 写入冲突时等待3秒,避免立刻抛database is locked |
路径这一行是很多人在开发环境正常、发布后翻车的主要原因,后面避坑章节会展开。
2.3 从建表到CRUD:实例源码里的三层模式拆解
这套实例源码的结构一般分成三层:数据库访问层、实体类层、界面调用层。数据库访问层集中管理连接字符串和SQL命令,界面层不直接写SQL,而是调用封装好的方法。这样做的好处是换数据库、换驱动时只改一层,不用动界面代码。
看一个典型的插入操作:
using System.Data.SQLite; string insertSql = @" INSERT INTO inventory (item_code, item_name, quantity, updated_at) VALUES (@code, @name, @qty, @time);"; using var conn = new SQLiteConnection(connStr); conn.Open(); using var insert = new SQLiteCommand(insertSql, conn); insert.Parameters.AddWithValue("@code", "SKU-1001"); insert.Parameters.AddWithValue("@name", "六角螺栓"); insert.Parameters.AddWithValue("@qty", 200); insert.Parameters.AddWithValue("@time", DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")); int rows = insert.ExecuteNonQuery();这里用@code、@name这类参数占位符,而不是直接拼接字符串,两层意义:一是防止SQL注入,二是让SQLite正确识别类型边界。注意最后一行time参数,我故意转成了字符串,因为SQLite没有原生的datetime类型,System.Data.SQLite对DateTime的处理在不同版本上规则不一致,统一存成可排序的文本格式最稳。
查询操作返回结果集,通常配合DataSet或DataReader使用:
using var query = new SQLiteCommand( "SELECT id, item_code, item_name, quantity FROM inventory WHERE quantity > @min", conn); query.Parameters.AddWithValue("@min", 0); using var reader = query.ExecuteReader(); while (reader.Read()) { Console.WriteLine($"{reader["item_code"]} - {reader["item_name"]} - {reader["quantity"]}"); }ExecuteReader执行后返回的是前向只读的数据流,适合逐行处理大批量结果;如果数据量不大,也可以直接用SQLiteDataAdapter填充DataTable,再绑定到DataGridView或WPF的ItemsControl上。实例源码里这两种写法都有,核心区别就是读一条处理一条,还是先把全部结果放进内存。
3. 进阶用法:事务、参数化查询与多线程下的SQLite配置
能跑通增删改查只是入门。实际开发中,上万条数据导入、多线程写入、并发连接才是真正拉开差距的地方。这一章把实例源码里藏得比较深的三个进阶点拆开讲。
3.1 参数化查询:防注入只是最低要求
很多开发者以为参数化就是为了防SQL注入,其实在SQLite场景里,它还有另一个作用:避免类型错乱。
比如你写“WHERE quantity > 100”,看起来没问题,但如果你从界面上拿文本框值,直接拼成SQL字符串,用户输入“100 or 1=1”就出事了。用参数化之后,SQLite会按参数类型进行比较,不会出现“数字列和文本列比大小”的隐式转换。
参数化写法有一个坑要留意:AddWithValue虽然方便,但它是根据C#变量类型推断SQLite类型的。如果你传入的是整数,它可能推断成TEXT,导致索引失效。更稳的写法是显式声明参数类型:
var param = cmd.Parameters.Add("@qty", DbType.Int32); param.Value = 200;这样SQLite知道这一列是INTEGER,查询优化器能用上索引。字符串参数同理,声明DbType.String后再赋值,避免把数字全部按文本处理。实例源码里如果查询速度突然退化,先检查是不是AddWithValue推断类型造成的。
3.2 事务与批量写入:把万行插入从几秒压到零点几秒
SQLite每条写操作默认是自动提交的,也就是说每执行一条INSERT,它都要做一次日志写入、一次文件同步。如果循环一万次插入,就等于做了一万次磁盘同步,速度自然慢得离谱。
把写操作包进事务,是批量场景下最有效的优化方式:
using var conn = new SQLiteConnection(connStr); conn.Open(); using var tx = conn.BeginTransaction(); using var insert = new SQLiteCommand( "INSERT INTO inventory (item_code, item_name, quantity) VALUES (@code, @name, @qty)", conn, tx); for (int i = 0; i < 10000; i++) { insert.Parameters.Clear(); insert.Parameters.AddWithValue("@code", $"SKU-{i}"); insert.Parameters.AddWithValue("@name", "批量零件"); insert.Parameters.AddWithValue("@qty", i % 100); insert.ExecuteNonQuery(); } tx.Commit();事务提交前,所有写操作都缓存在内存中,最后统一写入一次,磁盘同步次数从一万次降到一次。一万条数据从这个写法里跑下来,耗时能从前面的几秒甚至十几秒压到一秒以内。
注意两个细节:事务对象要传进SQLiteCommand的构造函数,否则命令在事务外执行;循环里要调用Parameters.Clear(),否则参数会越积越多。
如果还嫌慢,可以配合PRAGMA synchronous=NORMAL,让同步频率降下来。但代价是断电或系统崩溃时,最近几次提交的数据可能丢失。对于库存记录这类敏感数据,同步级别不要乱降。
3.3 多线程读写:WAL模式与Busy Timeout的组合配置
SQLite的并发模型是“单写多读”:同一时间只允许一个连接写库,但读操作可以并行。默认的rollback journal模式下,读操作和写操作互相阻塞,多线程场景经常撞车。
实例源码里推荐的做法是开启WAL模式,也就是连接字符串里那段“Journal Mode=WAL”。开启后,写操作写到日志文件,读操作读主库文件,两者互不阻塞。
using var conn = new SQLiteConnection(connStr); conn.Open(); string walEnable = "PRAGMA journal_mode=WAL;"; using var pragma = new SQLiteCommand(walEnable, conn); pragma.ExecuteNonQuery();WAL模式开启后,数据库目录里会多出两个文件:appdata.db-wal和appdata.db-shm。这是正常现象,不是残留垃圾,不要手动删除。程序正常关闭时,这两个文件会自动合并回主库文件。
多线程场景里,每个线程应该创建自己的连接对象,不要在线程间共享同一个连接。System.Data.SQLite虽然有连接池,但同一个连接被两个线程同时使用时,命令执行顺序无法保证。我一般会在线程内创建连接、执行、释放,由连接池处理复用。
如果多个线程同时写,其中一个线程会拿到“database is locked”错误。连接字符串里“Busy Timeout=3000”就是为了解决这个:写冲突时先等最多3秒,而不是立刻报错。锁冲突仍然存在,但至少给了排队机会。
4. 避坑与常见问题:跑实例源码最容易翻车的五个点
实例源码能跑通不代表你能跑通。下面这五个问题是这套包最常见的翻车点,每条都按“现象、原因、解决”来写,对照排查会省很多时间。
4.1 SQLite.Interop.dll报“无法加载”或“不是有效的Win32程序”
现象:程序在自己电脑上正常运行,发布到别的机器上启动即报错,提示“无法加载DLL‘SQLite.Interop.dll’”或“不是有效的Win32应用程序”。
原因:System.Data.SQLite的原生DLL分x86和x64两套,分别放在bin目录下的x86和x64子文件夹里。发布时如果把整个目录结构打乱了,或者只拷贝了主程序,原生DLL就找不到;报“不是有效的Win32程序”则是项目平台目标与DLL架构不匹配,比如项目设为x86,却拷贝了x64的原生DLL。
解决:发布时确保x86、x64子目录和SQLite.Interop.dll一起随程序走。不要为了省空间只复制一个架构的DLL,因为你不知道目标机器是32位还是64位系统。如果目标机器确定是64位,就把项目平台目标改成x64,并只带x64文件夹。
4.2 频繁写入时遇到“database is locked”
现象:程序运行一段时间后,在批量插入或大量并发写时突然冒出一个异常:SQLite error 5: database is locked。
原因:最常见的是同一个连接对象上执行了一条查询,DataReader没有关闭,紧接着又去执行写操作。SQLite在读取过程中会持有阶段锁,写操作只能等待。还有一种情况是开启了WAL但没设置Busy Timeout,写锁冲突直接异常退出。
解决:读操作务必用完即释放,把reader包在using块里。连接字符串里加上Busy Timeout=3000,让冲突等待而不是立刻报错。如果多个线程都在写,检查是否真的需要并发写,SQLite对并发写支持很有限,单线程串行写往往更可靠。
4.3 相对路径在发布后失效,数据库写到系统目录
现象:开发时连接字符串写的是“Data Source=data.db”,调试一切正常。发布后数据库找不到,系统盘根目录或System32下却冒出一个data.db。
原因:相对路径是相对“当前工作目录”,而不是程序所在目录。双击运行时工作目录可能是程序目录,但通过服务启动、计划任务、其他程序调用时,工作目录可能就是系统目录。
解决:统一用AppDomain.CurrentDomain.BaseDirectory拼接数据库路径:
string dbPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "appdata.db");实例源码里如果没做这一步,拿到的第一时间就可以改掉。数据库文件放在程序目录是最直观的,至少用户能找到、能备份。
4.4 时间字段用AddWithValue塞DateTime,读出类型经常乱
现象:写入时传的是DateTime对象,读出来发现是字符串,或者排序结果完全不对,时间变成了一串数字。
原因:SQLite没有原生的日期时间类型,日期通常存成TEXT或INTEGER。AddWithValue自动推断类型时,有的版本推断成TEXT,有的版本推断成INTEGER,读写不一致自然出问题。
解决:约定成俗的方式是统一存成“yyyy-MM-dd HH:mm:ss”格式文本,写入前ToString,读取后按字符串解析。这样至少能保证可读性和排序性。
4.5 从CSV或Excel导入后数值列变成文本,SUM和排序全错
现象:用代码循环读取CSV然后插入SQLite,quantity列明明存的是数字,执行SUM(quantity)却返回0,排序也按字符串排。
原因:CSV读出来的一切都是字符串。AddWithValue把全部值都推断成TEXT,数字列在SQLite里存成了文本。SQLite对类型是宽松的,不会帮你报错,计算时就按文本处理了。
解决:插入前做一次结构化清洗,数字列先用Convert.ToInt32或long.TryParse转成真正的数值类型,再用参数化插入。不要指望SQLite自动推断列类型修复这个问题,建表时的INTEGER声明只能约束新插入值,已经存进去的脏文本照样按文本处理。
5. 从源码里提炼的验证手法:用一段自检脚本把玄学变可控
SQLite有一个很实用的特性:它本身内置了一套健康检查命令,实例源码里很多排查问题都用得上。我拿到这套包后的第一个动作,不是看界面代码,而是写一个自检程序,把数据库文件、驱动、日志模式一次检查完。
5.1 数据库自检四连:完整性、日志模式、SQLite版本与坏页
下面这段代码可以放在工具菜单里,也可以写成一个单独的小程序,每次发布前跑一遍:
private static bool SelfCheck(string dbPath) { using var conn = new SQLiteConnection($"Data Source={dbPath};Version=3;"); conn.Open(); using var integrityCmd = new SQLiteCommand("PRAGMA integrity_check;", conn); string integrity = integrityCmd.ExecuteScalar()?.ToString(); using var journalCmd = new SQLiteCommand("PRAGMA journal_mode;", conn); string journalMode = journalCmd.ExecuteScalar()?.ToString(); using var versionCmd = new SQLiteCommand("SELECT sqlite_version();", conn); string sqliteVersion = versionCmd.ExecuteScalar()?.ToString(); Console.WriteLine($"integrity={integrity}, journalMode={journalMode}, sqlite={sqliteVersion}"); return string.Equals(integrity, "ok", StringComparison.OrdinalIgnoreCase); }integrity_check是SQLite自带的完整性校验命令,返回ok就代表数据库页没有损坏。journal_mode返回当前日志模式,确认WAL是否真的生效。sqlite_version可以帮助你判断目标机器上的驱动版本是否太老,避免用了新版SQLite特性却不兼容。
日常使用时,还有一个小技巧:把数据库目录下的-wal和-shm文件是否定期消失作为判断条件。WAL模式下,这两个文件如果长期不合并回主库,说明程序可能存在连接没关闭的问题。
5.2 新环境先跑自检,再谈业务
这套方法的来源其实是一次教训。有次我帮一个同事排查某进销存Demo的发布问题,现象是“我本机数据正常,客户机器上老是丢记录”,动了一整天才定位到目标机器上SQLite驱动版本太老,WAL文件合并逻辑有差异。从那以后,我每次拿到一包源码,都会先强制走一遍这个自检流程,再去看业务代码。光照着业务调不到根因,先从环境和数据层把黑匣子打开,很多玄学问题当场就现形了。这个方法救了我好多次发布现场,希望帮到你。
本文还有配套的精品资源,点击获取