You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.28 12:19:49