ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

C# SQLite 图片存取实战:BLOB 写入读取与避坑指南

C# SQLite 图片存取实战:BLOB 写入读取与避坑指南 简介面向.NET开发者的C#与SQLite图片存取示例项目演示如何利用SQLite轻量级数据库完成图片二进制数据的写入与读取。核心代码涵盖创建SQLite连接、建立BLOB字段表、将本地图片转为字节数组插入以及通过SELECT查询取出数据并显示到PictureBox控件适合初学数据库操作或桌面文件管理的开发人员参考。压缩包共49个文件以C#源文件9个cs、解决方案与项目配置sln、csproj、config为主附有exe可执行程序、dll依赖库及png界面截图整体约2.11MB包含完整Visual Studio解决方案结构。目前已有2469人学习下载示例包含可直接运行的窗体程序通过界面选择本地图片上传至数据库再从数据库读取显示可对照代码理解SQLite参数化查询与图片类型转换的常见思路。1. C# 把图片塞进 SQLite存取本身不难难的是存完读不出来把图片直接塞进 SQLite 数据库在 C# 上位机项目里几乎是绕不开的需求——设备拍照存档、检测结果留底、工单图片记录全都要落到本地库。可你要是拿 SQLite 存过图片就会发现存取本身不难难的是存完读不出来读出来是 0 字节、Image.FromStream直接抛参数无效、WPF 里Image控件死活不显示。这些坑我全踩过这篇就是一份能照抄的 C# SQLite 图片存取实战笔记从建表、参数化写入、字节流读取到缩略图分页每段代码都能直接粘进你的上位机项目里改一改就跑通。适合刚接触数据库存图片的新手也适合想直接抄参数的老手。2. 环境与工程搭建从 NuGet 包到 SQLite 文件的正确打开方式2.1 选包对比System.Data.SQLite 与 Microsoft.Data.Sqlite 怎么选存图片这件事首先卡在选包上。C# 连接 SQLite 常用的就两个库System.Data.SQLite和Microsoft.Data.Sqlite。两者都能完成 BLOB 字段的读写但行为差异很大选错后面会多出很多不必要的调试时间。对比项System.Data.SQLiteMicrosoft.Data.Sqlite底层原生 C 库需带 SQLite.Interop.dll纯托管实现基于 SQLitePCL.rawAPI 风格传统 ADO.NET支持 DbType.Binary 隐式转换轻量只有 SQLiteParameter原生依赖32/64 位需分开部署无需额外部署NuGet 自动处理适合场景WinForms、老项目、需要完整 ADO.NET 功能WPF、跨平台、新项目我的建议是如果你的项目是 WinForms 上位机、现场机器可能还是 Win7用System.Data.SQLite更稳因为它的功能更全遇到问题网上资料也多。如果是 WPF 新项目直接上Microsoft.Data.Sqlite部署省心SQLitePCL.raw会在构建时自动带上对应运行时的原生库。安装命令就看你自己项目了一个命令的事这里不占篇幅。装完之后记得检查项目输出目录里有没有SQLite.Interop.dllSystem.Data.SQLite 包特有没有的话运行时百分百报Unable to load DLL这个问题在第 5 章专题说。2.2 连接串与建库建表一句话让数据库文件自动出现SQLite 的特点是连接串指向一个本地文件文件不存在时第一次Open()会自动创建。下面是典型的建库建表代码using System.Data.SQLite; // 数据库文件放 exe 同级 Data 目录 var dataDir Path.Combine(AppDomain.CurrentDomain.BaseDirectory, Data); Directory.CreateDirectory(dataDir); var connStr $Data Source{Path.Combine(dataDir, images.db)};Version3;PoolingTrue;; using var conn new SQLiteConnection(connStr); conn.Open(); string sql CREATE TABLE IF NOT EXISTS Images ( Id INTEGER PRIMARY KEY AUTOINCREMENT, Name TEXT NOT NULL, ContentType TEXT NOT NULL, Width INTEGER NOT NULL DEFAULT 0, Height INTEGER NOT NULL DEFAULT 0, Data BLOB NOT NULL, CreateTime DATETIME DEFAULT CURRENT_TIMESTAMP );; using var cmd new SQLiteCommand(sql, conn); cmd.ExecuteNonQuery();这段代码里有几个参数值得说清楚DataSource是文件路径Version3是老版 SQLite 文件格式实际现在 3 以上就是标准格式写不写都行PoolingTrue开启连接池上位机频繁开关数据库的场景下能明显减少打开耗时。AUTOINCREMENT保证 Id 单调递增删除记录后不会复用旧 Id对需要在 UI 上按 Id 定位图片的场景很重要。表结构里Data BLOB NOT NULL是核心字段存的就是图片原始字节数组。Width和Height是冗余字段插入时顺手填上读取列表时可以只查这两个字段不查Data避免把整张图片的字节都捞出来列表页速度翻倍不止。建好表之后接下来就是写入环节。BLOB 字段写入的坑比读取更多最常见的错误是拿到一个byte[]就直接ToString()拼进 SQL 字符串这种行为在 SQLite 下会把字节数组转成类似System.Byte[]的字符串存进去读出来直接傻眼后面所有的反序列化逻辑全白搭。3. 图片写入的完整链路byte[]、BLOB 字段与参数化 SQL3.1 图片文件转换 byte[]文件、内存流两种来源的实现写入 SQLite 之前得先把图片变成byte[]。来源不同写法也不同。最常见的两种来源是磁盘文件和内存中的Image对象。// 方式一从磁盘文件直接读一步到位 byte[] fileBytes File.ReadAllBytes(C:\shots\camera_01.bmp); // 这种方式最简单但要注意图片文件可能被其他进程占用 // 方式二从内存中的 Image 对象转先转 MemoryStream 再取字节 using var ms new MemoryStream(); bitmap.Save(ms, System.Drawing.Imaging.ImageFormat.Jpeg); byte[] imageBytes ms.ToArray(); // 用 MemoryStream 中转的原因Image.Save 需要流对象而 byte[] 没有流的概念方式一适合的是上位机上已经落盘的图片文件比如工业相机保存的原始帧。方式二适合的是程序里自己绘制的图片比如把检测结果画框之后的内存 Bitmap。不管哪种来源最终都得到一个byte[]这就是准备写入 BLOB 字段的数据。有个细节要留意Image.Save(MemoryStream)之后得到的字节数组和原图字节不一定一致因为Save会按指定格式重新编码JPEG 会做有损压缩BMP 则体积膨胀。如果在意体积和速度建议直接用File.ReadAllBytes保持原始文件格式数据库存储的字节就是磁盘上的原始字节读出来可以直接落盘不用重新解码。3.2 参数化插入给 BLOB 传参时别让类型被当成整数BLOB 写入的正确姿势是参数化 SQL把byte[]通过参数对象传给命令让 ADO.NET 驱动去处理二进制映射。千万不要把字节数组转成字符串拼接 SQL这是新手最容易犯的错误。using var conn new SQLiteConnection(connStr); conn.Open(); const string sql INSERT INTO Images (Name, ContentType, Width, Height, Data) VALUES (name, contentType, width, height, data);; using var cmd new SQLiteCommand(sql, conn); cmd.Parameters.Add(new SQLiteParameter(name, DbType.String) { Value camera_01.bmp }); cmd.Parameters.Add(new SQLiteParameter(contentType, DbType.String) { Value image/bmp }); cmd.Parameters.Add(new SQLiteParameter(width, DbType.Int32) { Value 640 }); cmd.Parameters.Add(new SQLiteParameter(height, DbType.Int32) { Value 480 }); cmd.Parameters.Add(new SQLiteParameter(data, DbType.Binary) { Value fileBytes }); cmd.ExecuteNonQuery();这里最关键的参数是new SQLiteParameter(data, DbType.Binary)。DbType.Binary明确告诉驱动这个参数是二进制大对象驱动内部会把它映射成 SQLite 的 BLOB 存储类。如果把DbType.Binary漏掉或者写成DbType.Object在部分版本的 System.Data.SQLite 驱动下会把byte[]当成未知类型处理保存结果可能是 0 字节也可能是乱码这两种我都见过。Width和Height字段这里用了DbType.Int32目的是和表结构的 INTEGER 对应。这些字段虽然不影响 BLOB 存取但第 4 章做列表查询时要用插入时不填后面查询出来的宽度就是 0。3.3 事务批量写入一次塞进上百张图片的正确做法上位机做批量导入时比如把设备一整天拍的照片入库一张一张插入会有严重的性能问题。SQLite 单条插入默认走自动提交模式每条语句都要做一次完整的事务提交1000 张图就是 1000 次磁盘同步耗时奔着分钟级去。解决办法是手动开启事务把批量插入包在一个事务里。using var conn new SQLiteConnection(connStr); conn.Open(); using var transaction conn.BeginTransaction(); using var cmd new SQLiteCommand(sql, conn, transaction); // 把参数创建移到循环外只改 Value避免重复创建参数对象 var pName new SQLiteParameter(name, DbType.String); var pContentType new SQLiteParameter(contentType, DbType.String); var pWidth new SQLiteParameter(width, DbType.Int32); var pHeight new SQLiteParameter(height, DbType.Int32); var pData new SQLiteParameter(data, DbType.Binary); cmd.Parameters.AddRange(new[] { pName, pContentType, pWidth, pHeight, pData }); foreach (var file in Directory.GetFiles(C:\shots, *.bmp)) { var bytes File.ReadAllBytes(file); var img Image.FromFile(file); pName.Value Path.GetFileName(file); pContentType.Value image/bmp; pWidth.Value img.Width; pHeight.Value img.Height; pData.Value bytes; cmd.ExecuteNonQuery(); } transaction.Commit();参数对象在循环外创建一次、循环内只改Value这是批量写入性能的关键。SQLiteParameter对象本身有解析和绑定逻辑反复创建销毁会拖慢速度。我把这组代码放在离线导入工具里300 张 640x480 的 BMP 图事务包起来之后大概两秒左右落库而逐条插入要半分钟差别非常明显。还有一点血泪经验事务一定要包在try-catch里catch中执行transaction.Rollback()否则一旦循环中某条数据抛异常整个事务就悬挂在那连接池里的连接一直占用后续所有数据库操作全部阻塞。这个坑排查起来特别玄学日志看起来是死锁实际上是没回滚的事务堵着连接。4. 图片读取与还原DataReader 到 Bitmap 的四种姿势4.1 查询与字节流读取先取长度再定义长度读取 BLOB 字段比写入稍麻烦一点核心问题是byte[]长度的确定。SQLite 的 BLOB 字段在驱动里返回的是long类型直接强转成int在图片超过 2GB 时会溢出但实际场景中单张图片不会到那个量级所以更常见的坑反而出现在GetBytes的调用方式上。using var conn new SQLiteConnection(connStr); conn.Open(); const string sql SELECT Id, Name, Width, Height, Data FROM Images WHERE Id id;; using var cmd new SQLiteCommand(sql, conn); cmd.Parameters.Add(new SQLiteParameter(id, DbType.Int32) { Value 1 }); using var reader cmd.ExecuteReader(); if (!reader.Read()) return; // 推荐做法直接类型转换为 byte[] byte[] data (byte[])reader[Data]; // 备选做法用 GetBytes 分步读取适合超大 BLOB long blobLength reader.GetBytes(4, 0, null, 0, 0); // 先传 null 获取长度 byte[] buffer new byte[blobLength]; reader.GetBytes(4, 0, buffer, 0, (int)blobLength);两种做法我平时都写。推荐直接(byte[])reader[Data]一行代码拿到完整字节数组效率也最高。备选做法适合的是超大文件按块读取的场景比如一次读 1MB循环读完全部数据可以避免一次性分配几十 MB 的连续内存。但要注意两次GetBytes调用的fieldIndex参数也就是第一个参数传的是列序号从 0 开始数。上面 SQL 里Data是第 5 列所以传 4这个数字写错会拿到别的列的数据翻车的时候很难察觉。还有个常见误用是直接用reader[Data].ToString()之后再Encoding.UTF8.GetBytes()这等于把二进制经过了一次文本编码转换图片字节早就损坏了存进去的 BMP 读出来打不开这类问题几乎隔三差五就有人踩一次。4.2 WinForms 与 WPF 的图片还原差异一个用 Bitmap一个用 BitmapImage拿到byte[]之后还原成界面能显示的图片对象。WinForms 和 WPF 的处理方式完全不同混用会直接导致界面黑屏或者异常。// WinForms 还原方式 using var ms new MemoryStream(data); using var bitmap new Bitmap(ms); pictureBox1.Image (Bitmap)bitmap.Clone(); // 注意Bitmap.FromStream 生成的 Bitmap 会一直持有 ms 的引用 // 所以 ms 不能提前 using 释放要么不释放要么像上面这样 Clone 出来 // WPF 还原方式 var msWpf new MemoryStream(data); var bitmapImage new BitmapImage(); bitmapImage.BeginInit(); bitmapImage.CacheOption BitmapCacheOption.OnLoad; bitmapImage.StreamSource msWpf; bitmapImage.EndInit(); bitmapImage.Freeze(); // 跨线程使用必须 Freeze imageControl.Source bitmapImage;WinForms 里的Bitmap.FromStream(ms)有一个隐晦的行为Bitmap 对象在垃圾回收之前ms流不能被释放。如果ms被using提前释放后面pictureBox1.Image画图时会抛ArgumentException而且异常信息很模糊。我一般用Clone()复制出一份副本然后把原始 Bitmap 和 MemoryStream 都释放掉这样最干净。WPF 这边有两个重点。第一是CacheOption BitmapCacheOption.OnLoad告诉BitmapImage在EndInit()时立刻把流里的数据全部加载进内存否则它会等StreamSource的流被读取时才懒加载。第二是Freeze()把BitmapImage变成只读对象这样它才能跨线程使用。上位机里照片列表和详情页经常在不同线程刷新不Freeze()的话界面刷新时会抛InvalidOperationException这个异常信息会直接指向你调用Freeze()的地方。这两个还原方式封装成两个方法放在工具类里所有界面共用。WPF 项目尤其建议把这个封装好因为同一个byte[]转出来的BitmapImage和Bitmap在底层是两套完全不同的解码链路混用会出现显示异常但代码不报错的诡异现象。5. 避坑SQLite 存图片的五个踩坑记录5.1 存进去之后再查出来是 0 字节现象插入操作没有任何报错但用 DB Browser for SQLite 打开数据库文件看到Data字段是空的或者长度是 0。原因参数化 SQL 里给data传值时没有指定DbType.Binary驱动把它当成了字符串或未知类型处理。在 System.Data.SQLite 里未指定类型的参数在部分版本会走文本绑定路径byte[]被截断或丢弃。解决创建参数时强制指定DbType.Binary代码示例见第 3.2 节。如果你用了第三方的 ORM 框架比如 Dapper需要把参数声明成DbType.Binary而不是默认的DbType.Object。5.2 读取时 GetBytes 只返回了第一段数据现象图片能读出来但只有第一段字节是正确的后面的全丢图片打开发灰或者加载一半。原因GetBytes方法的buffer长度参数填错了。很多人第一次调用GetBytes(4, 0, buffer, 0, buffer.Length)以为能一次性读完但 BLOB 总长超过 buffer 长度时这个方法只会填满 buffer 然后返回实际读取长度并不会报错。解决先用GetBytes(4, 0, null, 0, 0)拿到实际长度long再创建对应长度的buffer最后调用完整的GetBytes。或者干脆不用GetBytes直接用(byte[])reader[Data]类型转换后者不需要关心长度问题。5.3 WPF 里 Image 控件加载后不显示图片现象代码执行了Source赋值了运行不报错但界面上的图片区域是空白的。原因BitmapImage的CacheOption没设置成OnLoad。默认的Default缓存策略会导致流对象在EndInit()之后不再保留数据等 WPF 渲染器要去读的时候StreamSource已经被释放或数据已经不可用。解决BeginInit()之后必须设置bitmapImage.CacheOption BitmapCacheOption.OnLoad;并且在EndInit()之后调用Freeze()。这两个属性是 WPF 显示图片的黄金组合少一个都会出问题。5.4 运行时找不到 SQLite.Interop.dll 导致连接失败现象程序启动后第一次连接数据库直接抛DllNotFoundException: Unable to load DLL SQLite.Interop.dll。原因System.Data.SQLite 包需要对应架构的SQLite.Interop.dll。新式 SDK 风格项目会自动把依赖复制到输出目录但老式非 SDK 项目或者手动引用 DLL 的项目需要手动把x86和x64目录下的 Interop 文件复制到运行目录。现场机器如果是 64 位系统但程序编译成AnyCPU驱动会优先加载 x64 Interop缺了就报错。解决项目配置改成明确的目标平台比如 x64多数上位机推广机器都是 64 位然后检查输出目录x64子文件夹里有没有SQLite.Interop.dll。没有的话去 NuGet 包目录里复制进去。这也是我用Microsoft.Data.Sqlite替换老库的原因后者不需要手工处理 Interop。5.5 同一天多次写入后数据库文件越来越大现象图片数量没增加多少但 .db 文件体积翻倍删除图片后文件大小也没有变化。原因SQLite 删除数据只是标记不会自动归还磁盘空间给操作系统。频繁插入、删除、更新 BLOB 字段数据库文件会留下大量空白页体积持续膨胀。同时PoolingTrue会让连接长时间占用VACUUM 操作也需要在无连接的情况下执行。解决定期执行VACUUM命令重组数据库文件。做法是把所有连接关闭后执行new SQLiteCommand(VACUUM, conn).ExecuteNonQuery()。注意 VACUUM 执行期间数据库文件会临时膨胀到原来的两倍大小磁盘剩余空间不够时会执行失败现场部署时给数据库所在磁盘留足余量。6. 进阶缩略图分离存储与图片分页读取6.1 缩略图双表设计列表页几百条不卡把原图直接读出来渲染成列表图片一大直接卡死。更合理的做法是双表设计原图表存完整 BLOB缩略图表只存一个小尺寸图片数据列表页只查缩略图。const string createThumbSql CREATE TABLE IF NOT EXISTS Thumbnails ( Id INTEGER PRIMARY KEY AUTOINCREMENT, PhotoId INTEGER NOT NULL REFERENCES Images(Id), Width INTEGER NOT NULL, Height INTEGER NOT NULL, Data BLOB NOT NULL );; // 插入原图后生成缩略图并入库 using var thumbMs new MemoryStream(); var thumb new Bitmap(img, new Size(200, 150)); thumb.Save(thumbMs, System.Drawing.Imaging.ImageFormat.Jpeg); const string insertThumbSql INSERT INTO Thumbnails (PhotoId, Width, Height, Data) VALUES (photoId, width, height, data);; var thumbCmd new SQLiteCommand(insertThumbSql, conn, transaction); thumbCmd.Parameters.Add(new SQLiteParameter(photoId, DbType.Int32) { Value newId }); thumbCmd.Parameters.Add(new SQLiteParameter(width, DbType.Int32) { Value 200 }); thumbCmd.Parameters.Add(new SQLiteParameter(height, DbType.Int32) { Value 150 }); thumbCmd.Parameters.Add(new SQLiteParameter(data, DbType.Binary) { Value thumbMs.ToArray() }); thumbCmd.ExecuteNonQuery();缩略图列表页只查Thumbnails表每张图撑死几 KB加载 500 条也就十几 MB 内存。用户双击某一张再按Id去查原图表这样详情页和列表页彻底解耦。6.2 分页读取与 Debug 可视化验证列表页数据量上来之后没有分页就会出现滚轮一滚就卡顿的情况。配合 SQLite 的LIMIT/OFFSET做分页每页 50 条足够流畅。const string pageSql SELECT t.Id, t.PhotoId, t.Width, t.Height, t.Data FROM Thumbnails t ORDER BY t.PhotoId DESC LIMIT pageSize OFFSET offset;; using var cmd new SQLiteCommand(pageSql, conn); cmd.Parameters.Add(new SQLiteParameter(pageSize, DbType.Int32) { Value 50 }); cmd.Parameters.Add(new SQLiteParameter(offset, DbType.Int32) { Value pageIndex * 50 });分页参数用参数化 SQL 传入不要拼接字符串。OFFSET越大查询会扫描越多的索引页这块 SQLite 就这么设计数据量在几万条以内不用纠结坚持用主键Id排序分页就行。做图片存取功能时我习惯在 Debug 模式下加一个可视化验证读取出来的byte[]直接写到临时目录并用系统图片查看器打开或者用 WinForms 的PictureBox在测试窗体里显示。这样能快速区分问题是出在写入端还是读取端。最后说个我自己定的规矩每次写完图片存取代码强制走一遍「写入 → 读回 → 对比字节」三步验证。从 SQLite 读出的字节和写入前的原始字节逐位比较完全一致才算通过。从那以后我做的几个上位机项目图片存取这块再没出过前端图片打不开的事故。希望帮到你。本文还有配套的精品资源点击获取
返回列表