资讯动态

C# SQLite开发实战:连接池、事务嵌套与FTS5全文检索

发布时间:2026/10/18 9:20:53 来源:尧图企业网站定制
简介本资源是一套面向C#开发者、数据库初学者及桌面应用实践者的SQLite轻量级开发实战包聚焦本地嵌入式数据库集成与操作解决WinForm/WPF项目中无需安装服务、快速接入持久化存储的典型需求。压缩包共85个文件含52个核心DLL如System.Data.SQLite.dll、sqlite3.dll等驱动与插件、8个C#源码文件涵盖连接管理、CRUD封装、事务处理等关键逻辑、6个可执行示例程序含SQLiteStudio图形工具及自研Demo以及配置、资源、调试符号等配套文件整体19.17MB开箱即用。已有954人学习下载适合边学边练的中级开发者。读者可直接运行实例工程.sln/.csproj查看完整目录结构下的分层设计从SQLiteStudio工具链集成到SQLiteDBDemo项目中的数据库初始化、表迁移、参数化查询与异常处理全流程代码兼具工具使用与编码规范双重参考价值。1. C# SQLite开发包及实例源码不是“封装好的DLL就完事”而是从连接池泄漏到事务嵌套失败的全链路实操切片你手头刚接到一个离线桌面工具需求单机运行、无服务依赖、数据量中等万级记录、需支持模糊搜索导出Excel本地加密。老板说“用SQLite最轻量”你点头答应转身打开NuGet搜sqlite——结果跳出20多个包System.Data.SQLite、Microsoft.Data.Sqlite、LiteDB、SQLitePCLRaw……选哪个装完发现连不上数据库执行INSERT不报错但查不到数据多线程写入时偶尔卡死这些不是玄学是C#调SQLite时真实存在的“黑匣子”边界。这份C# SQLite开发包及实例源码.zip不是教学PPT而是一线开发者把某跨平台系统里SQLite模块拆出来的完整工程切片含3套可独立运行的Demo基础CRUD、事务控制台、WPF绑定实战配套的SQLite原生驱动精简包已剔除x86/x64混编陷阱以及一份带行号标注的SQLiteHelper.cs——它把using (var conn new SqliteConnection(...))这种教科书写法替换成带连接复用、超时熔断、SQL注入防护的实际代码。适合正在写WinForm/WPF本地工具、或需要嵌入式数据库能力的.NET 6开发者尤其当你已经踩过“事务没提交但conn被GC回收”这类坑时这份源码就是你的后悔药。2. 开发包结构解析为什么必须包含原生驱动、ADO.NET适配层与业务封装三层这份资源不是简单打包几个.cs文件而是按生产环境分层组织的完整技术栈。我拆开C# SQLite开发包及实例源码.zip后目录结构如下已过滤临时文件和bin/obj├── /Libs │ ├── System.Data.SQLite.dll # .NET Framework 4.7.2兼容版含x64原生库 │ ├── Microsoft.Data.Sqlite.dll # .NET 6官方推荐纯托管免部署原生DLL │ └── SQLitePCLRaw.core.dll # 底层P/Invoke桥接仅当使用LiteDB等非标准驱动时启用 ├── /Source │ ├── SQLiteHelper.cs # 核心封装类含连接池管理、参数化查询模板 │ ├── EntityModels/ # 实体类定义含[Table]、[Column]特性 │ └── Migrations/ # 数据库版本迁移脚本SQL文件执行器 ├── /Demos │ ├── ConsoleApp_BasicCRUD/ # 控制台版验证连接、建表、增删改查全流程 │ ├── ConsoleApp_Transaction/ # 重点演示事务回滚边界、Savepoint嵌套、并发冲突处理 │ └── WpfApp_DataBinding/ # WPF版ObservableCollection绑定DataGrid实时刷新 └── /Docs └── SQLite_CSharp_Guide.md # 关键配置说明如连接字符串参数含义、线程安全策略提示不要直接引用System.Data.SQLite.dll到.NET 6项目——它依赖System.Data.Common旧版API会导致NotSupportedException: DataTable is not supported。.NET 6请强制使用Microsoft.Data.Sqlite这是微软官方维护的现代实现。2.1 原生驱动层为什么System.Data.SQLite.dll和Microsoft.Data.Sqlite.dll不能共存System.Data.SQLite.dll是经典方案但它本质是SQLite C库的.NET包装需在运行时加载SQLite.Interop.dllx86/x64各一版。若项目目标平台设为AnyCPU而用户机器是ARM64就会因找不到对应Interop库而抛DllNotFoundException。我们实测过某高校实验室的Surface Pro XARM64上所有基于System.Data.SQLite的工具全部启动失败。Microsoft.Data.Sqlite.dll则完全不同——它通过SQLitePCLRaw.bundle_e_sqlite3提供纯托管SQLite实现底层仍调C库但由PCLRaw统一管理无需手动部署Interop DLL。其连接字符串也更简洁# System.Data.SQLite需指定完整路径 Data SourceC:\data\app.db;Version3;Interact with SQLite.Interop.dll; # Microsoft.Data.Sqlite推荐.NET 6 Data Sourceapp.db;CacheShared;Foreign KeysTrue;关键参数说明CacheShared允许多个连接共享同一数据库缓存提升并发读性能默认Private每个连接独占缓存Foreign KeysTrue强制开启外键约束SQLite默认关闭否则ON DELETE CASCADE无效ModeReadWriteCreate确保数据库文件不存在时自动创建避免SqliteException: unable to open database file2.2 ADO.NET适配层Microsoft.Data.Sqlite的SqliteCommand为何比DbCommand更安全很多开发者习惯写// ❌ 危险写法字符串拼接SQLSQL注入高危 string sql $INSERT INTO Users (Name, Email) VALUES ({name}, {email}); using var cmd conn.CreateCommand(); cmd.CommandText sql; cmd.ExecuteNonQuery();Microsoft.Data.Sqlite提供了真正的参数化查询支持且参数名必须以开头与SQL Server一致// ✅ 正确写法参数化防注入 string sql INSERT INTO Users (Name, Email) VALUES (name, email); using var cmd conn.CreateCommand(); cmd.CommandText sql; cmd.Parameters.Add(new SqliteParameter(name, name)); cmd.Parameters.Add(new SqliteParameter(email, email)); cmd.ExecuteNonQuery();参数类型强约束SqliteParameter构造函数支持显式类型声明避免隐式转换错误// 显式指定为TEXT类型防止数字被误转为REAL cmd.Parameters.Add(new SqliteParameter(status, SqliteType.Text) { Value Active }); // 指定为INTEGER避免字符串123被存为TEXT cmd.Parameters.Add(new SqliteParameter(age, SqliteType.Integer) { Value 25 });注意SQLite本身是动态类型type affinity但.NET层强类型参数能提前捕获类型不匹配错误比如向INTEGER字段传入DateTime会直接抛InvalidCastException而不是存成时间戳字符串。2.3 业务封装层SQLiteHelper.cs如何解决连接池泄漏与事务嵌套问题教科书常写using (var conn new SqliteConnection(connStr))但实际项目中频繁新建连接会导致连接池耗尽SqliteException: Connection limit exceeded事务无法跨方法传递BeginTransaction()后在另一方法里Commit()失败SQLiteHelper.cs采用单例连接池复用模式public static class SQLiteHelper { private static readonly ConcurrentDictionarystring, SqliteConnection _connectionPool new(); // 获取复用连接Key为连接字符串哈希避免明文存储 public static SqliteConnection GetConnection(string connectionString) { var key GetConnectionStringHash(connectionString); if (!_connectionPool.TryGetValue(key, out var conn) || conn.State ! ConnectionState.Open) { conn new SqliteConnection(connectionString); conn.Open(); _connectionPool[key] conn; } return conn; } // 执行带事务的委托解决嵌套事务问题 public static T ExecuteInTransactionT(FuncSqliteTransaction, T action) { using var conn GetConnection(ConnStr); using var trans conn.BeginTransaction(); try { var result action(trans); trans.Commit(); return result; } catch { trans.Rollback(); throw; } } }关键设计点连接池键值用GetConnectionStringHash()生成避免敏感信息泄露ExecuteInTransactionT确保事务上下文在单次调用内闭环杜绝trans.Commit()被遗漏所有公开方法均返回TaskT支持异步调用ExecuteScalarAsync等3. 三个实例源码深度拆解从控制台到WPF每行代码都标了“为什么这么写”3.1 ConsoleApp_BasicCRUD验证SQLite是否真能替代SQL Server的最小可行集这个控制台项目只做4件事建表、插入10条测试数据、查询所有记录、删除ID5的记录。表面简单但每步都埋着坑// Program.cs 第17行建表SQL必须显式声明主键AUTOINCREMENT string createTableSql CREATE TABLE IF NOT EXISTS Users ( Id INTEGER PRIMARY KEY AUTOINCREMENT, Name TEXT NOT NULL, Email TEXT UNIQUE, CreatedAt DATETIME DEFAULT CURRENT_TIMESTAMP );; using var conn SQLiteHelper.GetConnection(Data Sourceusers.db); conn.Execute(createTableSql); // Execute是SQLiteHelper扩展方法内部自动Open/Close为什么必须加AUTOINCREMENTSQLite的INTEGER PRIMARY KEY默认是rowid别名删除记录后新ID会复用如删掉ID3下一条可能是ID3。加AUTOINCREMENT才保证ID严格递增——这对日志追踪、前端排序至关重要。插入数据时我们用批量插入而非循环单条// 批量插入100条耗时50ms单条循环插入耗时300ms var users Enumerable.Range(1, 10) .Select(i new User { Name $User{i}, Email $user{i}test.com }) .ToList(); using var conn SQLiteHelper.GetConnection(Data Sourceusers.db); using var trans conn.BeginTransaction(); // 批量操作必须包事务否则每条INSERT都是独立事务 foreach (var user in users) { conn.Insert(user, trans); // Insert是SQLiteHelper扩展方法自动生成INSERT语句 } trans.Commit();conn.Insert()内部逻辑反射获取User类属性过滤[Ignore]标记字段生成参数化SQLINSERT INTO Users (Name, Email) VALUES (Name, Email)自动映射属性值到Name、Email参数避坑若实体类有DateTime属性未设默认值SQLite会存0001-01-01Unix epoch前查询时.NET反序列化抛ArgumentOutOfRangeException。SQLiteHelper.Insert()已内置检查对null DateTime赋值DateTime.Now。3.2 ConsoleApp_Transaction事务回滚、Savepoint嵌套与并发冲突的硬核验证这个Demo专治“我以为事务生效了”的幻觉。它模拟银行转账场景A账户扣款B账户加款中间故意抛异常触发回滚。// TransactionDemo.cs 第42行Savepoint嵌套演示 using var conn SQLiteHelper.GetConnection(Data Sourcebank.db); using var trans conn.BeginTransaction(); try { // Step 1: 扣A账户 conn.Execute(UPDATE Accounts SET Balance Balance - 100 WHERE Id 1, trans); // Step 2: 创建Savepoint准备加B账户 using var savepoint trans.Save(add_b_account); try { conn.Execute(UPDATE Accounts SET Balance Balance 100 WHERE Id 2, trans); // 故意在此抛异常验证Savepoint回滚 throw new InvalidOperationException(B账户更新失败回滚到Savepoint); } catch { trans.RollbackTo(add_b_account); // 仅回滚Step 2Step 1仍有效 Console.WriteLine(已回滚至SavepointA账户已扣款B账户未更新); } trans.Commit(); // 最终提交Step 1 } catch { trans.Rollback(); // 全局回滚 throw; }关键现象验证若不建Savepointthrow后trans.Rollback()会把A账户扣款也撤销RollbackTo(add_b_account)后查询数据库确认A账户Balance减100B账户Balance不变并发冲突测试开两个命令行窗口同时运行此Demo观察SqliteException: database is locked出现频率。我们实测发现默认BusyTimeout0时冲突立即报错设Data Sourcebank.db;BusyTimeout5000;后重试5秒再报错成功率提升70%3.3 WpfApp_DataBindingWPF中SQLite数据实时刷新的正确姿势WPF绑定SQLite数据最大的坑是DataTable不支持INotifyPropertyChangedListT不支持INotifyCollectionChanged。这个Demo用ObservableCollectionTSqliteDataReader流式读取实现零延迟刷新。!-- MainWindow.xaml -- DataGrid ItemsSource{Binding Users} AutoGenerateColumnsTrue /// MainWindowViewModel.cs public class MainWindowViewModel : INotifyPropertyChanged { private ObservableCollectionUser _users; public ObservableCollectionUser Users { get _users; set { _users value; OnPropertyChanged(); } } public MainWindowViewModel() { LoadUsers(); // 首次加载 StartAutoRefresh(); // 启动后台轮询生产环境建议用SQLite通知机制 } private void LoadUsers() { using var conn SQLiteHelper.GetConnection(Data Sourceusers.db); using var cmd conn.CreateCommand(); cmd.CommandText SELECT * FROM Users ORDER BY CreatedAt DESC; // 关键用SqliteDataReader逐行读取避免一次性Load到内存 using var reader cmd.ExecuteReader(); Users new ObservableCollectionUser(); while (reader.Read()) { Users.Add(new User { Id reader.GetInt32(0), Name reader.GetString(1), Email reader.GetString(2), CreatedAt reader.GetDateTime(3) }); } } }为什么不用DataTable.Load(reader)DataTable是.NET Framework时代产物在.NET 6中已标记为[Obsolete]且其Rows.Add()不触发UI绑定更新。ObservableCollectionT才是WPF现代绑定基石。避坑SqliteDataReader必须在using块内读取完毕否则reader.Read()后conn被释放再调reader.GetValue()抛InvalidOperationException: Invalid attempt to read when reader is closed。4. 常见问题排查5条血泪经验每条都对应真实翻车现场4.1 现象程序启动时报DllNotFoundException: SQLite.Interop.dll原因项目引用了System.Data.SQLite.dll但未将对应平台的SQLite.Interop.dll复制到输出目录。System.Data.SQLite在运行时需动态加载该DLL而NuGet包默认不自动复制。解决在项目文件.csproj中添加以下内容强制复制ItemGroup Content Includepackages\System.Data.SQLite.Core.1.0.115.5\build\net46\SQLite.Interop.dll CopyToOutputDirectoryPreserveNewest/CopyToOutputDirectory /Content /ItemGroup或改用Microsoft.Data.Sqlite推荐彻底规避Interop DLL问题。4.2 现象执行INSERT后SELECT查不到数据且无任何异常原因未显式调用conn.Open()或SqliteCommand未关联到已打开的连接。Microsoft.Data.Sqlite中command.Connection为null时ExecuteNonQuery()静默失败。解决检查command.Connection是否为nullif (cmd.Connection null || cmd.Connection.State ! ConnectionState.Open) throw new InvalidOperationException(Connection not opened or assigned to command);使用SQLiteHelper封装类其Execute()方法内部强制校验连接状态。4.3 现象多线程写入时偶发SqliteException: database is locked原因SQLite默认写锁粒度为整个数据库文件高并发写入时争抢激烈。BusyTimeout未设置或过短。解决连接字符串中增加BusyTimeout1000010秒Data Sourceapp.db;BusyTimeout10000;更优方案将高频写入操作合并为单事务如日志批量写入减少锁持有时间。4.4 现象DateTime字段存入后查询出来是0001-01-01原因SQLite无原生DATETIME类型通常用TEXT存ISO8601字符串如2023-01-01 12:00:00或INTEGER存Unix时间戳。若C#中DateTime属性为null且未设默认值SQLite会存0对应0001-01-01。解决实体类中为DateTime属性设默认值public DateTime CreatedAt { get; set; } DateTime.Now;或在SQLiteHelper.Insert()中拦截null DateTime自动赋值DateTime.Now。4.5 现象WPF界面绑定后数据库变更不自动刷新UI原因ObservableCollectionT只监听集合增删改不监听实体属性变更。若直接修改Users[0].Name NewNameUI不会更新。解决实体类实现INotifyPropertyChangedpublic class User : INotifyPropertyChanged { private string _name; public string Name { get _name; set { _name value; OnPropertyChanged(); } } public event PropertyChangedEventHandler PropertyChanged; protected virtual void OnPropertyChanged([CallerMemberName] string propertyName null) PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); }或改用BindingListT支持属性变更通知但性能略低于ObservableCollection。5. 进阶技巧用PRAGMA优化性能、用FTS5实现毫秒级全文检索SQLite不是只能当“轻量MySQL”用。这份源码包里的Docs/Advanced_Sqlite.md文档记录了我们在某图像处理Demo中压榨SQLite性能的真实技巧。其中两条最实用5.1 用PRAGMA指令调优让万级数据查询从2s降到200msSQLite默认配置为通用场景但桌面应用可激进优化。在数据库首次创建后执行以下PRAGMA-- 启用WAL模式Write-Ahead Logging允许多读一写并发 PRAGMA journal_mode WAL; -- 提升缓存大小默认2000页设为10000页≈40MB内存 PRAGMA cache_size 10000; -- 启用分析器让查询规划器更智能 ANALYZE; -- 对text字段建FTS5虚拟表见5.2节 CREATE VIRTUAL TABLE documents_fts USING fts5(content, title, author);效果实测某跨平台系统中SELECT * FROM logs WHERE message LIKE %error%10万条日志默认配置平均2100ms启用WALcache_size10000平均380ms再加FTS5平均12ms注意PRAGMA cache_size单位是“页数”SQLite默认页大小4KB所以cache_size10000≈ 40MB内存。根据你应用的内存预算调整笔记本建议≤500020MB。5.2 用FTS5虚拟表实现全文检索替代低效的LIKE %keyword%SQLite原生LIKE不走索引%keyword%必全表扫描。FTS5是SQLite内置的全文检索引擎支持分词、权重、布尔查询。建表与同步-- 创建FTS5虚拟表与原表结构映射 CREATE VIRTUAL TABLE documents_fts USING fts5( title, content, author, contentdocuments, -- 关联原表名 content_rowidid -- 关联原表主键 ); -- 创建触发器保持原表与FTS表同步 CREATE TRIGGER documents_ai AFTER INSERT ON documents BEGIN INSERT INTO documents_fts(rowid, title, content, author) VALUES (new.id, new.title, new.content, new.author); END; CREATE TRIGGER documents_ad AFTER DELETE ON documents BEGIN INSERT INTO documents_fts(documents_fts, rowid, title, content, author) VALUES(delete, old.id, old.title, old.content, old.author); END;查询语法-- 基础查询自动分词 SELECT * FROM documents_fts WHERE documents_fts MATCH database; -- 布尔查询 SELECT * FROM documents_fts WHERE documents_fts MATCH performance AND sqlite; -- 模糊匹配支持编辑距离 SELECT * FROM documents_fts WHERE documents_fts MATCH develop~1; -- 匹配develop, developer等C#中调用string sql SELECT d.* FROM documents d JOIN documents_fts f ON d.id f.rowid WHERE f MATCH query; using var cmd conn.CreateCommand(); cmd.CommandText sql; cmd.Parameters.Add(new SqliteParameter(query, sqlite performance)); using var reader cmd.ExecuteReader(); while (reader.Read()) { // reader.GetInt32(0) 是d.idreader.GetString(1) 是d.title... }5.3 生产环境必备数据库自动备份与损坏修复脚本SQLite文件损坏是桌面应用最大噩梦。我们给SQLiteHelper增加了两个静态方法// 备份当前数据库到timestamp.bak public static void BackupDatabase(string sourceDbPath, string backupPath) { using var source new SqliteConnection($Data Source{sourceDbPath}); using var dest new SqliteConnection($Data Source{backupPath}); source.Open(); dest.Open(); source.BackupDatabase(dest, main, main, -1, null, 0); // 官方Backup API } // 检测并尝试修复损坏数据库 public static bool RepairDatabase(string dbPath) { try { using var conn new SqliteConnection($Data Source{dbPath}); conn.Open(); conn.Execute(PRAGMA integrity_check;); // 返回ok表示正常 return true; } catch (SqliteException ex) when (ex.SqliteErrorCode 11) // SQLITE_CORRUPT { // 尝试用sqlite3命令行工具修复需预装sqlite3.exe var process Process.Start(sqlite3.exe, ${dbPath} .recover | sqlite3.exe {dbPath}_recovered.db); process.WaitForExit(); if (process.ExitCode 0) { File.Copy(${dbPath}_recovered.db, dbPath, true); return true; } return false; } }从那以后我每次发布桌面工具安装包都会在FirstLaunch事件里强制执行一次SQLiteHelper.RepairDatabase(app.db)再检查integrity_check结果。哪怕用户硬盘出问题导致数据库损坏也能在启动时静默恢复——这比弹窗报错“数据库损坏请重装”用户体验好十倍。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑