Rust写入图片Blob到SQLite成功但读取失败问题求解
Rust操作SQLite存储Blob字段问题解答
问题更新记录
- 第一次更新:附上
Cargo.toml依赖配置供参考:
serde = { version = "1.0.117", default-features = false } serde_json = "1.0.66" sql-builder = "3.1" sqlite = "0.26.0"
- 第二次更新:添加调试代码
println!("{:?}", row[2].kind());后,输出结果为String,怀疑是Blob存储方式错误,不确定是否应该使用serde_json::to_string处理二进制内容。
原始问题
编写Rust操作SQLite的练手代码时,尝试将本地图片以Blob格式存入SQLite,再读取出来写入本地文件,运行时在读取Blob、调用as_binary()的位置触发panic,报错信息如下:
thread 'main' panicked at 'called `Option::unwrap()` on a `None` value', src/main.rs:51:38
通过VSCode SQLite查看器、DBeaver等工具查看库文件,确认数据已经写入表中,需要找到正确实现Blob读写的修改方案。
原始问题代码如下:
use std::{fs::File, io::{Read, Write}}; use sql_builder::{quote, SqlBuilder}; use sqlite::Connection; fn main() { // 数据库文件创建在 ./tmp/sqlite.db let conn = Connection::open("./tmp/sqlite.db").unwrap(); // 建表 conn.execute( "CREATE TABLE IF NOT EXISTS icon ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, content BLOB, used STRING )", ) .unwrap(); // 读取本地图片文件,准备作为Blob存入数据库 let mut file = File::open("./tmp/in.jpg").unwrap(); let mut contents = Vec::new(); file.read_to_end(&mut contents).unwrap(); println!("{:?}", contents); // 构造插入SQL let sql = SqlBuilder::insert_into("icon") .fields(&["name", "content", "used"]) .values(&[ quote(serde_json::to_string("in").unwrap()), quote(serde_json::to_string(&contents).unwrap()), quote(serde_json::to_string("1").unwrap()), ]) .sql().unwrap(); // 执行插入 conn.execute(&sql).unwrap(); // 从数据库读取图片写回本地 let mut builder = SqlBuilder::select_from("icon"); builder.field("id"); builder.field("name"); builder.field("content"); builder.field("used"); let stmt = conn.prepare(&builder.sql().unwrap()).unwrap(); let mut cursor = stmt.into_cursor(); let row = cursor.next().unwrap().unwrap(); let id = row[0].as_integer().unwrap(); let name = row[1].as_string().unwrap(); let content = row[2].as_binary().unwrap(); // 此处触发panic let used = row[3].as_string().unwrap(); println!("{} {} {}", id, name, used); let mut file = File::create("./tmp/out.jpg").unwrap(); file.write_all(content).unwrap(); }
问题根因
你的调试结果已经定位到核心问题:存入content字段的根本不是二进制Blob,而是JSON序列化后的字符串。
serde_json::to_string处理Vec<u8>类型时,会把字节序列转换成JSON数组格式的字符串,比如[123,24,52,...]这类文本,再经过quote()包裹后,SQLite会把这段内容当成TEXT类型存入。哪怕建表时声明字段是BLOB类型,SQLite的类型亲和性规则也会优先按传入值的实际类型存储,所以读出来的值类型是String,调用as_binary()自然会返回None,触发unwrap panic。
另外你对name和used字段的处理也属于多此一举,普通文本不需要先经过serde_json序列化再quote,直接传字符串即可。
修复方案
核心修改原则
插入Blob时不要用serde序列化二进制内容,使用参数化查询绑定二进制值。sql-builder只负责拼接SQL字符串,没法正确处理二进制参数,直接把二进制拼接到SQL里不仅会存成字符串,还存在SQL注入风险,要使用sqlite库本身的参数绑定能力传递二进制值。
修正后的完整代码
use std::{fs::File, io::{Read, Write}}; use sql_builder::SqlBuilder; use sqlite::{Connection, Value}; fn main() { let conn = Connection::open("./tmp/sqlite.db").unwrap(); conn.execute( "CREATE TABLE IF NOT EXISTS icon ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, content BLOB, used TEXT )", ) .unwrap(); // 读取本地图片 let mut file = File::open("./tmp/in.jpg").unwrap(); let mut contents = Vec::new(); file.read_to_end(&mut contents).unwrap(); // 构造插入SQL,用?作为参数占位符 let sql = SqlBuilder::insert_into("icon") .fields(&["name", "content", "used"]) .values(&["?", "?", "?"]) .sql().unwrap(); // 绑定参数执行插入,二进制内容直接传Binary类型 let mut insert_stmt = conn.prepare(&sql).unwrap(); insert_stmt.bind(1, "in").unwrap(); insert_stmt.bind(2, Value::Binary(contents)).unwrap(); insert_stmt.bind(3, "1").unwrap(); insert_stmt.next().unwrap(); // 查询数据 let mut builder = SqlBuilder::select_from("icon"); builder.field("id"); builder.field("name"); builder.field("content"); builder.field("used"); let stmt = conn.prepare(&builder.sql().unwrap()).unwrap(); let mut cursor = stmt.into_cursor(); let row = cursor.next().unwrap().unwrap(); let id = row[0].as_integer().unwrap(); let name = row[1].as_string().unwrap(); let content = row[2].as_binary().unwrap(); let used = row[3].as_string().unwrap(); println!("{} {} {}", id, name, used); // 写回文件 let mut file = File::create("./tmp/out.jpg").unwrap(); file.write_all(content).unwrap(); }
关键修改点说明
- 去掉所有入库值的
serde_json::to_string和quote调用,普通文本直接绑定即可,二进制内容用Value::Binary包装后绑定,保证存入数据库的类型是真实的BLOB。 - 插入语句改用
?作为占位符,通过参数绑定传值,避免SQL拼接带来的类型错误和注入风险。 - 建表时把
used字段的类型从STRING改成TEXT,SQLite原生没有STRING类型,虽然不影响运行,但使用标准类型更规范。
内容的提问来源于stack exchange,提问作者test1696150
相关产品推荐
相关产品推荐

