做 .NET 桌面应用和离线工具,只要数据量一上来,就躲不开“本地数据库”这个坎。我这些年接手过的项目里,有 WinForms 的进销存、WPF 的生产看板、给产线用的离线质检工具,还有偏移动端的 .NET MAUI 原型,全都能碰到本地 Db 数据库技术方案选型的问题。今天我就把常用的一套组合拳——SQLite、LocalDB、LiteDB——放在同一张工作台上拆开讲,包括它们各自的适用场景、落地实现、加密方式、并发限制和坑点,给正在选型或者准备改型的同学一份可以直接抄作业的参考。
这文章不是纯科普,更像是我实际踩过一轮后的项目复盘。你会看到“当时为什么这么选”“试了之后发现哪里不对”“换方案后怎么解决”这类真实记录,也会看到可以直接粘贴使用的连接串、初始化代码和迁移策略。适合谁看?准备做 WinForms/WPF/.NET MAUI 离线应用、需要内嵌数据库但还没定方案的朋友,以及已经在用某个本地库但总觉得别扭、想换个思路的开发者。
1. 本地数据库选型这事,别急着敲代码
1.1 为什么桌面和离线应用绕不开本地库
先说一个看似低级但其实很多人卡住的点:为什么不能直接序列化成 JSON 或者 XML 文件存硬盘?小型配置可以,一旦数据量过万、需要按条件查询、需要事务保证一致性,文件方案就开始拉胯。你手动写一个索引试试,或者模拟一次“写了一半断电”,就知道什么叫崩溃。更常见的是多窗口、多进程同时读写的协作问题,自己维护锁和增量更新,代价比想象中大得多。
所以本地数据库的价值从来不是“存数据”,而是三件事:结构化查询、事务保障、并发控制。.NET 平台的好处是生态成熟,不管选哪类数据库,都能找到对应的 ADO.NET Provider 或 EF Core Provider,接入成本低。坏处则是选择太多,容易纠结,甚至有人把 SQL Server 的思维直接套到本地库上,最后被部署问题和资源占用教训一顿。
1.2 选型前先问清楚这 5 个问题
我建议在打开 NuGet 页面之前,先拿一张纸回答以下问题,答案会帮你自动过滤掉一半方案。
第一,数据模型是强关系型还是灵活结构?如果业务里面有大量固定表结构、外键关联、复杂 Join 查询,文档型数据库会让你写得很痛苦;反过来,如果数据是嵌套结构、字段经常变,关系型数据库的迁移也会让你头疼。
第二,单用户还是多用户并发?本地数据库分两类:一类是面向单进程嵌入的(SQLite/LiteDB),一类是面向轻量服务的(LocalDB/Express)。多用户写并发超过一定量级,前者的设计上限会变成瓶颈。
第三,部署环境是可控还是不可控?你的客户端是公司内部固定机器,还是外部用户的随机环境?后者必须考虑安装包大小、是否需要额外运行时、数据库文件能否嵌入 exe 目录。
第四,数据安全和备份要求是什么?本地库一般不需要像服务端那么强的权限体系,但加密、备份、恢复一定要想清楚,别等下个项目上线才补。
第五,团队和维护成本。团队要是已经精通 SQL Server,选 LocalDB 的学习成本趋近于零;要是本来就在用 EF Core,SQLite 的接入最顺滑;要是团队就你一个人写工具,LiteDB 那种傻瓜式 API 反而能提速。
2. 三套主流方案实测对比:SQLite、LocalDB、LiteDB
2.1 SQLite:关系型场景的首选默认项
SQLite 在 .NET 生态里几乎是默认选项了。EF Core 官方提供 Microsoft.EntityFrameworkCore.Sqlite,底层是 Microsoft.Data.Sqlite,整个体系由微软自己维护,版本跟进非常快,.NET 8/9 用得很舒服。
它的特点大家多少听过:单文件、零配置、跨平台。对 WinForms 和 WPF 来说,发布的时候只需要带一个 .db 文件(甚至一开始可以不建文件,代码里自动生成),不需要装服务,也不占内存进程,这对客户端应用太友好了。我做过一个产线工具,要求免安装绿色版,整个程序加数据库文件加起来不到 20MB,拷到工控机上就能跑,这就是 SQLite 的典型优势。
但注意,SQLite 不是万能药。它的并发模型是“单写多读”,虽然 WAL 模式能显著改善读写并发,但多个进程同时写基本还是会锁库。另外它对 ALTER TABLE 的支持很弱,很多迁移操作实际上是重建表,数据量大时很痛苦。所以它适合中小数据量、单机、查询频繁但不要求高并发写的场景。
2.2 LocalDB/SQL Server Express:需要完整 SQL Server 能力时
LocalDB 和 Express 是一条路线上的两个档位。LocalDB 是轻量级、按需启动的 SQL Server 实例,一般用于开发环境;Express 则可以部署到客户端机器上作为独立数据库服务,有完整 T-SQL、存储过程、视图、触发器。
如果你所在的企业本来就重度依赖 SQL Server,代码里全是存储过程和复杂 SQL,那么选 LocalDB/Express 可以零成本复用团队经验。我见过某 MES 项目,服务端用 SQL Server,客户端离线模式也要求同样的数据结构和校验逻辑,他们最后就选 Express 做本地缓存,同步代码完全一套逻辑打通,省了很多事。
代价也很明显:部署要装东西,安装包体积大,实例管理和连接串配置比嵌入式方案复杂,在低配工控机上跑服务进程会增加内存和 CPU 消耗。如果你只是要一个轻量本地库,没必要上这套,不然会被“服务没起来”“实例连接不上”这类问题拖死。
2.3 LiteDB:纯 .NET 的文档型轻量选择
LiteDB 是一个纯 .NET 实现的嵌入式文档数据库,理念类似 MongoDB,但运行在进程内。它对 .NET 开发者非常友好,不需要额外原生依赖,一个 DLL 搞定,支持 BSON 文档、LINQ 查询、索引、事务,还自带密码加密。
我用 LiteDB 做过一个配置管理工具,数据本身是树形的,一层套一层,用关系表存得来回拆表和拼装,用文档存就一行序列化。还有一个小型工单流转系统,字段经常加,LiteDB 不需要迁移表结构,直接往文档里塞字段就行,柔性特别好。
不过 LiteDB 的并发和复杂查询能力要比 SQLite 弱一截,数据量大、多线程写频繁时性能下降明显。它更适合中小规模、结构灵活、以读写单个文档为主的应用,或者作为配置/日志存储,而不是核心业务数据仓库。
我直接给一张对比表,方便对照。
| 维度 | SQLite | LocalDB/Express | LiteDB |
|---|---|---|---|
| 嵌入方式 | 进程内嵌入 | 独立服务进程 | 进程内嵌入 |
| 数据文件 | 单文件 .db | 文件组/MDF | 单文件 .db |
| 并发写能力 | 弱,单写者 | 强,完整服务能力 | 弱,适合轻并发 |
| 复杂 SQL/存储过程 | 有限支持 | 完整支持 | 不支持 SQL |
| EF Core 支持 | 官方 Provider | 官方 Provider | 社区 Provider |
| 部署复杂度 | 极低 | 高 | 极低 |
| 加密 | SQLCipher 或 SEE | TDE/文件加密 | 内置密码加密 |
| 最佳场景 | 桌面单机、移动端 | 企业复杂业务离线缓存 | 灵活结构和轻量配置 |
3. 实操落地:我用 EF Core + SQLite 走通的一套流程
3.1 项目初始化与依赖安装
默认推荐方案,我项目里是 WinForms 项目。开箱第一步,装三个包:Microsoft.EntityFrameworkCore.Sqlite、Microsoft.EntityFrameworkCore.Tools,外加一个可选但建议装的 Microsoft.EntityFrameworkCore.Design。版本要和目标 .NET 版本对齐,比如 .NET 8 就装 8.x,别拿到 9.x 往上硬怼,容易撞兼容问题。
包装好后,定义一个常规 DbContext。我这里给个简化版示例:
public class AppDbContext : DbContext { public DbSet<Product> Products { get; set; } protected override void OnConfiguring(DbContextOptionsBuilder options) { options.UseSqlite("Data Source=app.db"); } }这个写法能跑通 demo,但正式项目不建议把连接串写在 OnConfiguring 里。更好的做法是放在构造函数传参,或者用 ConfigurationBuilder 从配置文件读,后面要切换数据库提供程序或者做测试的时候方便得多。
3.2 数据库文件路径与连接串配置
连接串里 Data Source 的值可以是绝对路径、相对路径,也可以是文件 URI。很多人开发时很爽,发布完用户一跑就报“unable to open database file”,十有八九是路径问题。默认相对路径是当前工作目录,WinForms 双击 exe 和从命令行启动、在服务里启动,工作目录可能都不一样,所以路径必须显式确定。
稳妥的做法是把数据库放在用户的 AppData 目录,或者程序的基目录。
var dbPath = Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "MyApp", "app.db"); Directory.CreateDirectory(Path.GetDirectoryName(dbPath)!); var connectionString = $"Data Source={dbPath}";用 AppData 的好处是用户有完整读写权限,不会因为安装在 Program Files 下面导致无权限创建数据库文件。可执行文件放在只读目录的场景,比如 MSIX 打包或绿色软件放在 U 盘上,更要避免写文件到程序目录。
3.3 迁移与初始化:EnsureCreated 之外的选择
初期赶进度,很多人喜欢用 Database.EnsureCreated(),这个方法在数据库不存在时按模型建库,很快,但有个致命坑:它不会记录任何迁移历史,以后模型一改动,想升级表结构就只能删库重建。本地工具可以接受,业务系统绝对不能这么做。
我第二次重构就直接上 EF Core Migrations 了。流程是:
dotnet ef migrations add Init dotnet ef database update然后程序启动时,执行迁移以确保客户端数据库版本和代码模型匹配。
using (var db = new AppDbContext()) { db.Database.Migrate(); }Migrate() 会在首次运行时创建数据库并应用所有迁移,后续模型更新只需发布新版本时附带新的迁移文件,已在用户机器上的数据库就能自动升级,不用人工处理。这个体验对客户端应用非常重要,我后来所有带本地库的 WinForms/WPF 项目都采用了启动时自动迁移策略。
3.4 并发与性能:WAL 模式与连接管理
SQLite 默认的 journal mode 是 DELETE,读和写互相排队,桌面应用可能没感觉,但多线程频繁读写的程序会有卡顿感。我第一次做 WPF 看板时遇到 UI 卡顿,排查后发现问题出在 SQLite 在每次写时频繁加锁、刷磁盘。解决办法是启用 WAL 模式。
PRAGMA journal_mode=WAL; PRAGMA busy_timeout=3000; PRAGMA foreign_keys=ON;这三行建议在每次打开连接时执行。WAL 模式下读写可以并行,写事务通过 busy_timeout 等待锁而不是直接报错,外键约束默认关闭的坑也一并解决。EF Core 的 SQLite Provider 会在连接字符串支持 Cache=Shared 的情况下复用连接,但你还是需要注意连接生命周期。
一个很重要的实践是:桌面应用尽量用短连接,即需要时打开、用完立刻释放。不要像写服务端那样养一个长连接池。原因很简单:SQLite 的写锁粒度是数据库文件级别,长连接持有事务会挡住其他进程的写入;短连接配合 WAL 模式,反而能降低锁冲突概率。
4. 加密与安全:本地数据库的另一半功课
4.1 SQLCipher 加密 SQLite
很多人不知道,默认的 SQLite 数据库文件是明文,任何文本编辑器打开都能看到表结构和数据。做进销存、医疗、政务类软件,数据合规一定会要求加密。SQLite 生态对应的方案是 SQLCipher,在 .NET 里通过 Microsoft.Data.Sqlite.Core + SQLitePCLRaw.bundle_e_sqlcipher 使用。
安装方法不复杂:
<PackageReference Include="Microsoft.Data.Sqlite.Core" Version="..." /> <PackageReference Include="SQLitePCLRaw.bundle_e_sqlcipher" Version="..." />连接串里加一个 Password:
Data Source=app.db;Password=你的密钥;要注意,SQLCipher 用的是加密文件格式,已有未加密数据库无法通过在连接串加密码直接变成加密库,必须先新建加密库再导入数据。加解密交接过程建议用工具脚本,别在业务代码里硬怼。
加密带来的性能开销是真实存在的。我做过一个压测,同样批量插入一万条记录,无加密耗时大约 1.2 秒,SQLCipher 加密后接近 1.7 秒,写放大明显。对桌面应用来说这个量级可以接受,但你要心里有数,别等上了生产才发现性能瓶颈。
4.2 LiteDB 的加密机制
LiteDB 加密就简单直接,连接串带 Password 即可,底层是 AES 加密整个数据文件。
Filename=app.db;Password=myPassword;它不像 SQLCipher 需要换原生库,API 层面零改动。如果你选择 LiteDB 就是冲着轻量去的,那么它的内置加密会帮你省不少事。但同样要记住,加密只保护静止状态的数据文件,程序运行时的内存数据是不受保护的。
4.3 本地数据库加密的常见误区
我把见过的低级错误集中说一遍。第一,密钥硬编码在代码里,这等于把保险柜钥匙贴在柜子上。客户端程序一般建议用 DPAPI 加密密钥后存在本地,或者通过配置系统注入,别明文写死。
第二,只加密数据库文件但没加密日志和临时文件。SQLite 在 WAL 模式下会有 -wal 和 -shm 文件,这些文件里也可能有明文数据,所以加密方案要确保整个数据库家族文件都被覆盖。
第三,忘了备份。加密数据库一旦密钥丢失,数据就永久不可读。我就遇到过同事把密钥写在本地的一个 txt 里,结果磁盘坏道把 txt 毁了,业务库也打不开的情况。现在我的习惯是密钥文件至少放两个独立存储,并定期测试从备份恢复。
5. 上线前后最容易踩的坑与排查实录
5.1 文件被锁定与并发冲突
SQLite 最经典报错就是 database is locked(或 SQLITE_BUSY)。很多人以为这是数据库坏了,其实大概率是多进程或多线程同时写。我做个工单系统时就踩过:WinForms 主窗口和后台日志线程各开了连接,偶尔就锁一下。
排查思路首先是看代码里有没有并发写事务。优化方案有三个:一是把写操作放到统一队列串行执行;二是开 WAL 模式并设置 busy_timeout;三是短连接加即时事务。
如果确实需要跨进程共享同一个本地库文件,比如别的小工具也想访问,我建议加一层重试逻辑。
for (var attempt = 0; attempt < 3; attempt++) { try { await using var db = new AppDbContext(); // 执行写操作 await db.SaveChangesAsync(); break; } catch (SqliteException ex) when (ex.SqliteErrorCode == 5) { await Task.Delay(200 * (attempt + 1)); } }5.2 路径和部署环境不一致
这个坑我在 3.2 提过,这里展开讲一个实际案例。有一版工具,开发机上跑得好好的,交付给客户后只要从计划任务启动就报“unable to open database file”。最后发现计划任务的默认工作目录是 system32,相对路径 app.db 自然指向了一个根本没有写权限的地方。改成绝对路径写到 AppData 后问题消失。
另外,MSIX 打包的软件目录是虚拟化的,写程序目录会被系统重定向,表现特别诡异。排查这类问题最快的方法是:程序启动时立刻输出一个日志文件,记录当前 Environment.CurrentDirectory 和实际使用的数据库路径,让现场反馈日志,比远程盲猜快得多。
5.3 原生依赖与运行时的坑
SQLite 虽然号称纯原生,但实际通过 SQLitePCLRaw 加载的是各平台原生库。发布 WinForms/WPF 时容易遇到“无法加载 SQLite.Interop.dll”或 DllNotFound,多数时候是原生库没被复制到输出目录。新版 Microsoft.Data.Sqlite 一般会自动处理,但如果你手欠改了 RuntimeIdentifier,或者用了单文件发布,就要检查一下输出的 runtimes 目录是否完整。
这个场景还遇到过用户机器缺 .NET 运行时的情况:安装包没带 runtime,系统又是旧 Windows,直接弹“This application requires one of the following versions of the .NET Framework”。解决方案没有捷径,要么自包含发布,要么把 runtime 合并在安装流程里。.NET Framework 3.5 那类老组件问题也差不多,都是部署文档要提前写清楚的事项。
5.4 EF Core 迁移引发的“意外删数据”
还有一个高发事故:没配迁移,模型改了一字段,EnsureCreated 只判断“库是否存在”,不会判断“表结构是否匹配”,结果就是启动时直接报“no such column”。更痛的是某些版本还会建议你删除数据库重建。我在一个演示项目里就干过一次重建操作,客户本地录入了几天的数据直接没了。
所以再次强调,凡是数据要长期保留的项目,别用 EnsureCreated。哪怕觉得迁移麻烦,也要在模型稳定后第一时间切换到迁移模式。真出现本地数据库损坏或者记录丢失,优先尝试其他手段,比如导出旧的 SQLite 数据文件,绝不轻易执行删除重建动作。
6. 选型决策速查表与后续扩展
6.1 一眼看懂怎么选
常见场景对应的推荐方案,我整理成下面几张场景卡片。
| 项目类型 | 推荐方案 | 原因 |
|---|---|---|
| WinForms 进销存 | EF Core + SQLite | 关系明确,部署简单,EF 生态成熟 |
| WPF 数据看板 | EF Core + SQLite(WAL) | 查询频繁,读写并发可控 |
| .NET MAUI 移动离线应用 | EF Core + SQLite | 跨平台,官方支持 |
| 企业复杂离线缓存 | LocalDB/Express | 与 SQL Server 一致,复用团队经验 |
| 树形配置/工单柔性模型 | LiteDB | 文档模型灵活,加字段免迁移 |
| 日志型小工具 | LiteDB | 写入简单,可加密 |
6.2 根据项目规模决定深度
如果是单机工具或内部小软件,我认为 SQLite 就够了,不需要引入 LocalDB,也不需要过早做复杂的加密和迁移设计。控制在使用成本:连接串、建表、CRUD、WAL 模式,半天时间能跑通。
如果是多用户、数据一致性要求高的业务系统,但客户端又必须离线运行,我会优先评估 SQLite 加队列写是否满足。不满足再考虑 LocalDB/Express。这里有个经验阈值:单库文件并发写超过 5 个线程、或者日增数据量超过 10 万条,就不要再硬抗 SQLite 了,直接考虑服务型数据库。
如果是数据模型频繁变化的创新型项目,LiteDB 能帮你缩短迭代周期。我有个项目最早用 SQLite,每次改字段都要加迁移,后来换 LiteDB,模型直接序列化存储,开发效率翻了一倍。
6.3 我的个人建议和维护心得
从我的实际情况来说,现在默认方案是:桌面端首选 EF Core + SQLite,特殊需求才换 LiteDB 或 LocalDB。理由只有一个,EF Core 的生态和资料最丰富,团队接手成本最低,遇到问题能搜到现成答案。数据库本身不是架构核心,稳定和可维护才最重要。
最后分享一个我在本地数据库方案踩坑后养成的小习惯:每个项目都会在启动时记录数据库文件路径和版本号,一旦用户反馈数据异常,先看日志定位文件位置,再备份旧库,然后才谈修复。如果你正准备上本地数据库,也可以提前把这套诊断机制加进去。
本地库选型本身没有“标准答案”,但把业务场景、部署环境、团队能力这三件事想清楚,选起来就不会太纠结。希望这篇复盘能帮你少走一圈弯路,真要动手时少一点“为什么我之前没发现”的后悔。