☰
C#上位机图片存储:SQLite BLOB存取实践指南
2026/9/25 14:10:09 网站建设 项目流程

简介:示例工程面向需要学习 C# 与轻量级数据库 SQLite 交互的 .NET 初学者,核心解决图片数据如何以二进制形式落库并回读的问题。SQLite 作为一种无服务器、自包含的嵌入式数据库,非常适合桌面工具与本地数据缓存场景,本资源示范了在 WinForms 项目中通过 ADO.NET 接口操作它的完整流程。工程围绕 SQLiteTest 项目展开,完整演示了建立数据库连接、创建含 BLOB 字段的数据表、将 JPG/PNG 图片读取为字节数组后参数化插入,以及用 SQLiteDataReader 取出数据并显示到 PictureBox 控件的全过程,同时包含环境依赖与项目配置。压缩包共包含49个文件,以 C# 源文件(.cs)、WinForms 界面相关文件、数据库依赖库(.dll)和可执行程序(.exe)为主,还附带项目配置文件与数据库文件,整体大小仅 2.11MB,便于直接打开调试。已有2469人学习下载,说明该方法在日常桌面工具和小型应用中较有参考价值。通过此工程,读者不仅能看到完整可运行的窗口程序,还能掌握参数化查询、字节数组转换、临时文件加载等实用写法,为后续开发图片管理或本地缓存类工具提供代码基础。

1. C# 搭配 SQLite 存图片:为什么说这条路值得走

做过 C# 上位机或者桌面工具的人,迟早会遇到一个问题:采集到的图片、截图、相机帧,到底该往哪里放?直接丢文件系统,几十上百个文件散落在目录里,备份靠拷贝,检索靠文件名,一旦和业务数据脱钩,后期维护就是一场灾难。把图片塞进 SQLite,让字节流和业务记录在同一个数据库文件里,备份一个文件就带走全部数据,查询、清理、迁移都变得干净利落。这就是“C#使用SQLite存取图片的示例”这个标题背后真正的价值——不是教你怎么存一张图,而是给你一套在桌面应用、上位机系统里管理图片数据的可靠方案。

适合谁?写 WinForms、WPF 桌面工具的人,用 C# 做工业上位机、需要把相机抓拍和检测结果一起落地的工程师,以及被文件系统折磨过、想把图片纳入事务管理的开发者也适合这条路。SQLite 的本地文件特性让图片数据随库而走,不需要额外部署数据库服务,单机场景下这是最轻、最稳的选择。我用这套方案在工控机上存过相机抓拍图,几百毫秒的写入时间对生产节奏完全无感。下面从库的选择、表结构设计、核心代码到坑位排查,一条线讲透。

2. 先想清楚再动手:SQLite 存图片的底层逻辑与选型

2.1 为什么偏偏是 SQLite 而不是 MySQL 或直接存文件

桌面应用和上位机场景里,数据库选型的首要约束往往不是性能,而是部署成本和运维复杂度。MySQL 需要安装服务、配置账号、处理网络连接,放在工控机上本身就是额外的故障点;直接存文件虽然省事,但文件路径和业务数据的关联全靠开发者自觉,一套系统跑三个月,目录结构乱到没人敢动。SQLite 是嵌入式关系数据库,整个数据库就是一个 .db 文件,C# 通过 System.Data.SQLite 或 Microsoft.Data.Sqlite 访问它,不需要独立进程,不需要账号密码,开箱即用。

图片存进 SQLite 的本质,是把图片文件的二进制内容读成 byte[],然后作为 BLOB 类型写入数据库。读取时再把 byte[] 转回 Image 对象或直接写回文件。这条路能用,关键在于 SQLite 对 BLOB 的支持足够成熟,而且事务机制保证写入的原子性——图片数据和对应的业务记录要么一起成功,要么一起回滚,不会出现图存了记录丢了这种半截账。性能上,单张几百 KB 的图片写入耗时在毫秒级,完全满足桌面应用和多数工业采集场景。

2.2 两种驱动怎么选:System.Data.SQLite 与 Microsoft.Data.Sqlite

C# 访问 SQLite 的主流驱动有两个:System.Data.SQLite 是官方提供的完整版,包含原生 SQLite 引擎,支持加密扩展,但安装包偏大;Microsoft.Data.Sqlite 是微软写的轻量实现,基于 SQLitePCLRaw,依赖更干净,对 .NET Core / .NET 5+ 支持更好。我的习惯是:写 .NET Framework 的老项目用 System.Data.SQLite,写 .NET 6+ 的新项目一律 Microsoft.Data.Sqlite。

两个驱动在 API 使用上很接近,都遵循 ADO.NET 的套路:创建连接、打开连接、创建命令、执行命令。差异主要藏在连接字符串和参数前缀上,System.Data.SQLite 用 @ 前缀参数,Microsoft.Data.Sqlite 同样用 @,但如果你的代码跑在旧版上,偶尔会碰到参数名大小写敏感的问题。下面示例以 Microsoft.Data.Sqlite 为主,因为当前 C# 项目基本都在往现代 .NET 靠。

2.3 表结构设计:图片字段的常用形态与隐含成本

存图片的表结构建议独立设计,不要把所有字段堆在一张表里。推荐的做法是业务表和图片表分开,用业务主键关联。假如你有一个产品检测记录表,图片表大致长这样:

CREATE TABLE inspection_images ( id INTEGER PRIMARY KEY AUTOINCREMENT, inspection_id INTEGER NOT NULL, image_data BLOB NOT NULL, image_type TEXT NOT NULL, capture_time TEXT NOT NULL, remark TEXT );

这样设计有三个好处:图片数据独立存储,不影响业务表的查询性能;一个检测记录可以关联多张图片,符合实际场景;删除业务记录时按关联 ID 清理图片,逻辑清晰。字段上要注意,image_data 是核心,建议加上 NOT NULL 约束,防止写入空数据。image_type 存格式后缀,比如 jpg 或 png,读取时用来还原文件格式。

代价也要说清楚:SQLite 单文件承载图片后,文件体积会快速膨胀,VACUUM 之前不会自动收缩。另外,把图片读进内存时,如果一次性加载几十张,内存占用会很难看。所以表结构设计好之后,读写逻辑里必须带上分批和按需加载的意识,后面代码部分会展开。

3. 把图片写进 SQLite:最小可运行代码与参数说明

3.1 项目准备:NuGet 包引入和连接字符串要点

新建一个 .NET 6+ 的 Console 项目或者 WinForms 项目,先装包。Visual Studio 里打开 NuGet 包管理器,或者直接命令行:

dotnet add package Microsoft.Data.Sqlite

装完之后,连接字符串最简单的写法是:

var connectionString = "Data Source=images.db";

这里唯一必须传的参数是 Data Source,指向数据库文件路径。路径可以是相对路径,程序启动目录下会生成 images.db;也可以写绝对路径。当你的程序是上位机软件、需要部署到别的机器时,推荐用相对路径,或者用 AppContext.BaseDirectory 拼一个固定目录,避免因为当前工作目录变化导致数据库文件散落各处。我的做法是统一放到程序目录下的 data 文件夹,连接字符串写成:

var dbPath = Path.Combine(AppContext.BaseDirectory, "data", "images.db"); Directory.CreateDirectory(Path.GetDirectoryName(dbPath)); var connectionString = $"Data Source={dbPath}";

注意,SQLite 连接字符串里还有一个常用参数是 Pooling,默认开启。对于桌面应用,连接池可以少一点频繁开关连接的开销,但也要留意,长时间占用连接不放会导致数据库文件被锁。后续避坑章节会专门讲。

3.2 写入的核心代码:从图片文件到 BLOB 字段

写入的完整流程分三步:读文件字节 → 开连接 → 执行 INSERT。下面这段代码可以把一张本地图片存进 inspection_images 表:

using Microsoft.Data.Sqlite; using System.IO; // 1. 读取图片文件为字节数组 byte[] imageBytes = File.ReadAllBytes(@"C:\captures\sample.jpg"); // 2. 准备连接和参数化 INSERT 语句 using var connection = new SqliteConnection(connectionString); connection.Open(); using var command = connection.CreateCommand(); command.CommandText = @" INSERT INTO inspection_images (inspection_id, image_data, image_type, capture_time, remark) VALUES (@inspectionId, @imageData, @imageType, @captureTime, @remark);"; command.Parameters.AddWithValue("@inspectionId", 1001); command.Parameters.AddWithValue("@imageData", imageBytes); command.Parameters.AddWithValue("@imageType", "jpg"); command.Parameters.AddWithValue("@captureTime", DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")); command.Parameters.AddWithValue("@remark", "产线A相机抓拍"); // 3. 执行写入并返回受影响行数 int rows = command.ExecuteNonQuery(); Console.WriteLine($"已写入 {rows} 行");

逻辑说明:File.ReadAllBytes 把整张图片一次性读入内存,得到 byte[],这是 SQLite 存图的通用姿势。参数化写入是为了避免 SQL 注入,图片数据是二进制,拼字符串必炸,参数化是最稳的方式。AddWithValue 会自动把 byte[] 映射为 BLOB 类型,不需要手动声明 DbType。

参数说明:@inspectionId 关联业务表主键,@imageData 是核心图片数据,@imageType 存扩展名不要带点,@captureTime 存字符串格式的时间,方便直接排序查询。如果图片来自摄像头而不是文件,byte[] 可以从 MemoryStream 或 Bitmap 转换得到,后面进阶部分单独说。

3.3 从摄像头或内存图转 byte[]:一种常见姿势

上位机场景常遇到的是内存中的 Bitmap,而不是磁盘文件。Bitmap 转 byte[] 的代码不复杂,但容易踩编码格式的坑:

public static byte[] BitmapToBytes(Bitmap bitmap, string format = "jpeg") { using var ms = new MemoryStream(); // 指定图片格式,jpeg 体积小,png 保真度高 var imageFormat = format == "png" ? System.Drawing.Imaging.ImageFormat.Png : System.Drawing.Imaging.ImageFormat.Jpeg; bitmap.Save(ms, imageFormat); return ms.ToArray(); }

这段代码的坑在于:Bitmap.Save 到 MemoryStream 后,必须用 ms.ToArray() 取字节,不要用 ms.GetBuffer()。GetBuffer 返回的是底层缓冲区,长度可能大于实际数据,多出来的字节会影响写入结果。ToArray 是拷贝一份精确长度的数组,安全。格式选择上,工业抓拍图推荐 jpeg,因为原始位图转成 PNG 存储可能膨胀好几倍。

4. 从 SQLite 读出图片:还原、渲染与避免内存翻车

4.1 按条件查询一张图片并还原到文件

读图片的核心是执行 SELECT 拿到 BLOB 字节,再决定是写回文件还是显示到界面上。按 ID 查一张图并写出到文件:

public static void ExportImage(long imageId, string outputPath) { using var connection = new SqliteConnection(connectionString); connection.Open(); using var command = connection.CreateCommand(); command.CommandText = "SELECT image_data, image_type FROM inspection_images WHERE id = @id;"; command.Parameters.AddWithValue("@id", imageId); using var reader = command.ExecuteReader(); if (reader.Read()) { // 方式一:直接按字节读取 byte[] imageBytes = (byte[])reader["image_data"]; string type = reader["image_type"].ToString(); // 按原格式写回文件 string fullPath = $"{outputPath}\\{imageId}.{type}"; File.WriteAllBytes(fullPath, imageBytes); Console.WriteLine($"图片已导出: {fullPath}"); } reader.Close(); }

逻辑说明:ExecuteReader 返回 SqliteDataReader,通过索引器读取 image_data 字段并强转为 byte[]。这里有另外一个更推荐的方式,用 GetFieldValue 泛型方法,性能和类型安全都更好:byte[] imageBytes = reader.GetFieldValue<byte[]>(0)。强转写法在字段恰好为 NULL 时会抛异常,而 GetFieldValue 同样会抛,所以实践中一定要先判空。条件查询的 where 子句可以根据业务变,比如按 inspection_id 一次拿多张,但注意不要没完没了地把整个表载入内存。

4.2 在 WinForms / WPF 里显示图片:避免跨线程与 Dispose 坑

桌面应用里读出来的 byte[] 最终要显示到界面上。WinForms 的 PictureBox 显示代码常见写法是:

byte[] imageBytes = GetImageBytesFromDb(imageId); using var ms = new MemoryStream(imageBytes); var bitmap = new Bitmap(ms); pictureBox.Image?.Dispose(); // 释放上一张,防止内存泄漏 pictureBox.Image = bitmap;

关键细节在 MemoryStream 的释放和 Bitmap 的生存周期。Bitmap 是从 MemoryStream 构造的,很多人习惯把 ms 包在 using 里,结果 Bitmap 显示正常,但后续保存或再次使用会报“GDI+ 中发生一般性错误”,这正是因为 Stream 被提前释放了。稳妥做法有二:要么 ms 和 bitmap 都不 using,在 PictureBox 换图时统一 Dispose 旧对象;要么用 bitmap.Clone() 复制一份再释放 ms。我一般用第二种,代码更安全:

using var ms = new MemoryStream(imageBytes); using var original = new Bitmap(ms); var clone = (Bitmap)original.Clone(); // Clone 之后与 ms 无关 pictureBox.Image?.Dispose(); pictureBox.Image = clone;

另一个高频坑是跨线程操作控件。上位机里采集线程往往是后台线程,直接在后台线程里给 pictureBox.Image 赋值会抛 InvalidOperationException。解决方式是使用控件的 BeginInvoke 或一个 UI 线程调度器,把赋值动作丢回 UI 线程。如果项目用了 async/await,确保 await 之后的上下文仍处于 UI 线程,一般就没问题。

4.3 批量导出时怎样控制内存:分页查询与流式处理

一次查出几十上百张图片并全部写入内存,Bitmap 对象不及时 Dispose,内存轻轻松松飙到几 GB。批量导出场景推荐分页查询:

public static void ExportByInspection(long inspectionId, string outputDir) { using var connection = new SqliteConnection(connectionString); connection.Open(); using var command = connection.CreateCommand(); command.CommandText = @" SELECT id, image_data, image_type FROM inspection_images WHERE inspection_id = @inspectionId;"; command.Parameters.AddWithValue("@inspectionId", inspectionId); using var reader = command.ExecuteReader(); int count = 0; while (reader.Read()) { byte[] imageBytes = reader.GetFieldValue<byte[]>(1); string type = reader.GetFieldValue<string>(2); long id = reader.GetFieldValue<long>(0); string filePath = Path.Combine(outputDir, $"{id}.{type}"); File.WriteAllBytes(filePath, imageBytes); count++; } Console.WriteLine($"导出完成,共 {count} 张"); }

这里没有把所有的 byte[] 堆在内存里,而是读一张、写一张、释放一张。SqliteDataReader 默认是流式读取的,所以这样处理不会把整张表加载进内存。如果表里图片特别多,建议在 SQL 里加 LIMIT/OFFSET 或用游标分批处理,避免一个 reader 长时间占用数据库连接导致别的写入阻塞。批量导出时还要做一步:先解引用后强制 GC。注意不能频繁手动 GC,会导致性能抖动,在真正的大批量场景结束之后做一次回收是合理的。

5. SQLite 存图片的 5 个常见坑:现象、原因、解决

5.1 数据库文件被锁定:database is locked

现象:程序运行一段时间后,执行 INSERT 或 UPDATE 时报 SQLite Error 5: database is locked,尤其在多线程同时读写时高频出现。

原因:SQLite 使用文件锁实现并发控制,默认 journal mode 是 delete,多线程同时写同一个库文件会产生锁竞争。另一个常见原因是连接没有及时释放,using 块之外还有连接存活,事务没有提交。

解决:先检查所有 SqliteConnection 是否都包在 using 里。如果生产者线程(采集图片)和消费者线程(写入数据库)并发高,把 journal mode 调整为 WAL,它能显著减少读写锁冲突:

using var connection = new SqliteConnection(connectionString); connection.Open(); using var cmd = connection.CreateCommand(); cmd.CommandText = "PRAGMA journal_mode=WAL;"; cmd.ExecuteNonQuery();

注意,WAL 模式下数据库目录会多出 -wal 和 -shm 两个文件,备份时要一并拷贝。工控机异常断电后,-wal 文件可能残留,但 SQLite 恢复机制一般能自行处理。如果仍然频繁锁,检查是否有未提交事务,尤其在使用 BeginTransaction 后忘记 Commit。

5.2 数据库文件越用越大:delete 的数据没有真正释放

现象:删除图片记录后,数据库文件大小没有变小,甚至越来越大。

原因:SQLite 删除数据只是标记页为可复用,不会自动归还给操作系统。除非执行 VACUUM,否则文件空间不会收缩。图片不断增删后,文件里堆积大量空闲页。

解决:维护窗口里执行 VACUUM 命令:

using var connection = new SqliteConnection(connectionString); connection.Open(); using var cmd = connection.CreateCommand(); cmd.CommandText = "VACUUM;"; cmd.ExecuteNonQuery();

VACUUM 会重建整个数据库文件,耗时与数据量成正比,图片多的时候可能几十秒到几分钟,不要在业务高峰期跑。另一个策略是定期清理过期图片:先按时间删除记录,再执行增量 VACUUM。严格来说,图片这种只增不删的数据,存储规划要在设计阶段想清楚保留周期。

5.3 保存的图片损坏或无法预览:byte[] 被截断或格式丢失

现象:从数据库导出的图片文件打不开,或者文件大小明显偏小。

原因:常见于写入时用了 MemoryStream.GetBuffer() 而不是 ToArray(),导致写入的字节包含底层缓冲区的多余长度。另一个原因是 Bitmap.Save 到流之后,流的位置指针没有影响 ToArray(ToArray 从 0 开始),所以主要嫌疑还是 GetBuffer。还有一种情况是 image_type 写错,比如实际是 png 写成了 jpg,扩展名和后端格式不符,预览器打不开。

解决:统一用 ToArray(),导出时严格按照 image_type 字段拼接文件名。如果旧数据已经损坏,排查写入代码,修正后重新生成数据。这里也建议在写入前打印 byte[] 长度,和文件大小对比,立刻就能发现问题。

5.4 读取大量图片时界面卡死:UI 线程被数据库操作阻塞

现象:界面上点击“加载图片列表”后,窗口无响应几秒钟,图片多时更严重。

原因:数据库读取和图片解码都在 UI 线程同步执行,CPU 密集的字节拷贝和解压阻塞了消息循环。

解决:把数据库读取放到 Task.Run 或 async/await 里,只把最终的 Bitmap 通过 BeginInvoke 送到 UI 线程。一个参考做法是:后台线程按分页查出一批 id 和 byte[],解码成缩略图,再一次性提交到 UI。注意 Bitmap 是托管对象,但持有 GDI+ 句柄,跨线程赋值前确保原 Bitmap 不再被后台线程修改。

5.5 并发写入同一张图片导致脏数据:事务边界没控制好

现象:同一检测记录同时写入多张图片,部分图片丢失或关联错误。

原因:每个 INSERT 独立提交,也没有在同一事务里绑定业务主键。并发时事务交错,业务记录和图片的关联不一致。

解决:把图片写入和业务记录更新放进同一个事务:

using var connection = new SqliteConnection(connectionString); connection.Open(); using var transaction = connection.BeginTransaction(); using var cmd = connection.CreateCommand(); cmd.Transaction = transaction; cmd.CommandText = @" INSERT INTO inspection_images (inspection_id, image_data, image_type, capture_time) VALUES (@inspectionId, @imageData, @imageType, @captureTime);"; cmd.Parameters.AddWithValue("@inspectionId", 1001); cmd.Parameters.AddWithValue("@imageData", imageBytes); cmd.Parameters.AddWithValue("@imageType", "jpg"); cmd.Parameters.AddWithValue("@captureTime", DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")); cmd.ExecuteNonQuery(); transaction.Commit();

事务边界清楚了,写入失败时全部回滚,不会出现半张图入库的情况。工业上位机里有一类典型需求:相机抓到一张不合格品图,要连同检测结果一起存,事务就非常关键。用事务还能减少磁盘同步次数,批量导入几百张图时性能提升明显。

6. 进阶用法:缩略图缓存、加密与一套可复用的数据访问层

6.1 缩略图缓存:让列表查询不再每次加载原图

图片列表页面最大的性能瓶颈不是数据库,而是解码原图。一张 4000x3000 的 JPEG 大约 2MB,解码成 Bitmap 可能占用几十 MB 内存。如果列表要显示 50 条记录,按原图解码直接卡死。进阶做法是再建一张缩略图表,写入原图时同时生成 120x90 的小图存进去。生成缩略图的代码:

public static byte[] CreateThumbnail(byte[] sourceBytes, int width, int height) { using var ms = new MemoryStream(sourceBytes); using var original = new Bitmap(ms); using var thumbnail = new Bitmap(original, width, height); using var outMs = new MemoryStream(); thumbnail.Save(outMs, System.Drawing.Imaging.ImageFormat.Jpeg); return outMs.ToArray(); }

写入原图时多一条 INSERT,列表查询只读缩略图。这样列表加载快一个数量级,而点击大图时才加载原图。对于上位机的实时列表刷新,这个优化的收益立竿见影。另外,缩略图用 JPEG 而不是 PNG,体积小很多,列表分页时依然保持流畅。

6.2 SQLite 数据库文件能否加密:有条件但别迷信

很多人问 SQLite 数据库文件能否加密。默认的 SQLite 没有任何加密,Database 文件复制走就能被任意工具打开浏览图片数据。如果需要加密,有两个方向:使用 SQLCipher(System.Data.SQLite 的商业扩展版或 SQLCipher 原生库),或者自己在业务层对 byte[] 做 AES 加密后再写入。业务层加密的代价是无法在 SQL 里做基于内容的检索,但对于图片这种纯二进制对象,业务层加密完全够用。

我的建议是:数据敏感程度高、有合规要求时直接用 SQLCipher,连接字符串加上 password 参数;如果只是防普通用户拷贝,业务层 AES 是零依赖的轻量方案。不要把密码硬编码在代码里,放在配置文件并用 DPAPI 或环境变量保护。考虑到 SQLCipher 在 .NET 里的接入需要原生库,部署时要额外带上对应的 DLL,这一点比普通 SQLite 麻烦些。

6.3 封装一个图片存取仓储类:把连接管理收敛到一处

代码写多了,就会悟到一个教训:不要把 SqliteConnection 的创建散落在每个方法里。封装一个仓储类,统一管理连接字符串和基本操作,后续维护代价最低。下面给出一个精简版:

public class ImageRepository { private readonly string _connectionString; public ImageRepository(string connectionString) { _connectionString = connectionString; } public long Insert(long inspectionId, byte[] imageBytes, string imageType) { using var connection = new SqliteConnection(_connectionString); connection.Open(); using var command = connection.CreateCommand(); command.CommandText = @" INSERT INTO inspection_images (inspection_id, image_data, image_type, capture_time) VALUES (@inspectionId, @imageData, @imageType, @captureTime); SELECT last_insert_rowid();"; command.Parameters.AddWithValue("@inspectionId", inspectionId); command.Parameters.AddWithValue("@imageData", imageBytes); command.Parameters.AddWithValue("@imageType", imageType); command.Parameters.AddWithValue("@captureTime", DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")); return (long)command.ExecuteScalar(); } public byte[] GetImageData(long imageId) { using var connection = new SqliteConnection(_connectionString); connection.Open(); using var command = connection.CreateCommand(); command.CommandText = "SELECT image_data FROM inspection_images WHERE id = @id;"; command.Parameters.AddWithValue("@id", imageId); var result = command.ExecuteScalar(); return result as byte[]; } }

这个仓储类把连接创建、命令执行、参数绑定都收拢了。你可以在构造时注入连接字符串,也可以直接注入一个连接工厂。对于更大一点的项目,建议再套一层接口,方便以后换数据库或做单元测试时替换实现。这里注意 ExecuteScalar 返回类型是 object,转型前要判 null。

6.4 数据一致性验证:插入后立刻读回比对

写代码是一回事,验证数据没写坏是另一回事。我的习惯是每次写完核心功能,立刻做一个回读校验:插入后马上按 ID 查回 byte[],比对长度和哈希。哈希比对用 SHA256,长度比对可以作为第一道关卡:

// 插入后回读校验 byte[] written = GetImageData(newId); bool lengthOk = written.Length == originalBytes.Length; bool hashOk = SHA256.HashData(written).SequenceEqual(SHA256.HashData(originalBytes)); Console.WriteLine($"长度校验: {lengthOk},哈希校验: {hashOk}");

这一段在数据库操作写完之后花两分钟加进去,能挡掉绝大多数写入截断和格式错乱问题。批量导入时也可以做一个抽样校验,比如每 100 张取 1 张比对哈希。数据是给人用的,校验逻辑不是多余的自我感动。

6.5 关于备份的最后一个习惯

因为我吃过亏,所以多说一句。SQLite 存图片后,整个库里图片数据占比可能高达 95% 以上,备份就是把 .db 文件复制走。但如果你启用了 WAL 模式,直接拷贝 .db 文件可能丢失最新数据,正确做法是先执行一次 checkpoint:

using var connection = new SqliteConnection(connectionString); connection.Open(); using var cmd = connection.CreateCommand(); cmd.CommandText = "PRAGMA wal_checkpoint(TRUNCATE);"; cmd.ExecuteNonQuery();

执行完之后再拷贝数据库文件,-wal 和 -shm 里尚未合并的数据会先落盘。备份完成后可以顺手做一次完整性检查:

PRAGMA integrity_check;

返回 ok 说明数据库文件是健康的。这个习惯,让我少跑了不知道多少趟产线去修数据库。希望帮到你。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询