第11章:BLOB数据处理——存储与读取二进制数据、图片文件的数据库存储
说实话,BLOB(Binary Large Object)这玩意儿,我第一次接触是在做一个图片管理系统的时候。当时客户要求把用户头像直接存进数据库,我第一反应是「这靠谱吗?」后来实践证明,只要用对方法,SQLite处理二进制数据其实挺顺手的。
这一章,我们就来聊聊怎么用SQLite的BLOB类型存储和读取二进制数据,包括图片文件。我会结合sqlite3_bind_blob和sqlite3_column_blob这两个核心API,把我在项目中踩过的坑和积累的经验都抖出来。
11.1 BLOB是什么?为什么需要它?
BLOB,说白了就是「一大坨二进制数据」。它可以是一张图片、一个PDF文件、一段音频,甚至是一个序列化的对象。在SQLite中,BLOB类型专门用来存储这些非文本数据。
你可能会问:「为什么不直接存文件路径?」嗯,这个问题我当年也纠结过。存路径确实轻量,但缺点也很明显:文件移动了怎么办?备份时要同时备份数据库和文件,麻烦不?权限管理怎么做?
直接存BLOB,数据就全在数据库里了。备份一个文件搞定,迁移也简单。当然,代价就是数据库体积会变大,查询性能也会受影响。所以我的建议是:小文件(比如头像、缩略图)用BLOB,大文件(比如高清视频)还是存路径吧。
11.2 SQLite中的BLOB类型
SQLite对BLOB的支持很直接。建表时,字段类型写成BLOB就行:
CREATE TABLE user_avatar (
id INTEGER PRIMARY KEY,
user_name TEXT NOT NULL,
avatar BLOB, -- 头像二进制数据
upload_time TEXT
);
注意,SQLite是弱类型数据库,你写成BLOB、BINARY甚至不写类型,它都能存二进制数据。但为了可读性和维护性,我建议还是明确写上BLOB。
11.3 核心API:sqlite3_bind_blob
往数据库里写BLOB数据,用的是sqlite3_bind_blob。这个函数的原型长这样:
int sqlite3_bind_blob(
sqlite3_stmt *stmt, -- 预处理语句
int index, -- 参数索引(从1开始)
const void *data, -- 二进制数据指针
int n, -- 数据长度(字节数)
void (*destructor)(void*) -- 析构函数,通常传SQLITE_TRANSIENT
);
最后一个参数destructor,我当年第一次用的时候没搞明白,结果吃了大亏。它有三种常用取值:
| 取值 | 含义 | 使用场景 |
|---|---|---|
| SQLITE_STATIC | SQLite不会复制数据,直接使用你提供的指针 | 数据在bind之后不会改变,且生命周期足够长 |
| SQLITE_TRANSIENT | SQLite会内部复制一份数据 | 数据是临时变量,bind之后可能被释放 |
| 自定义回调 | SQLite在不再需要数据时调用此函数释放内存 | 需要精细控制内存管理时 |
我个人习惯:除非能100%保证数据指针的生命周期,否则一律用SQLITE_TRANSIENT。安全第一嘛。
11.4 实战:存储图片到数据库
假设我们要把一张PNG图片存进数据库。先读取文件到内存,然后bind进去:
// 读取图片文件到内存
FILE *fp = fopen("avatar.png", "rb");
if (!fp) {
fprintf(stderr, "打开文件失败\n");
return -1;
}
// 获取文件大小
fseek(fp, 0, SEEK_END);
long file_size = ftell(fp);
rewind(fp);
// 分配内存并读取
unsigned char *buffer = (unsigned char*)malloc(file_size);
if (!buffer) {
fclose(fp);
return -1;
}
size_t read_size = fread(buffer, 1, file_size, fp);
fclose(fp);
// 准备SQL语句
sqlite3_stmt *stmt;
const char *sql = "INSERT INTO user_avatar (user_name, avatar, upload_time) VALUES (?, ?, datetime('now'))";
sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
// 绑定参数
sqlite3_bind_text(stmt, 1, "张三", -1, SQLITE_STATIC);
sqlite3_bind_blob(stmt, 2, buffer, file_size, SQLITE_TRANSIENT);
// 执行
int rc = sqlite3_step(stmt);
if (rc == SQLITE_DONE) {
printf("图片存储成功!\n");
} else {
fprintf(stderr, "存储失败: %s\n", sqlite3_errmsg(db));
}
// 清理
sqlite3_finalize(stmt);
free(buffer); // 注意:因为用了SQLITE_TRANSIENT,这里可以安全释放
这里有个细节:free(buffer)放在sqlite3_finalize之后。虽然用了SQLITE_TRANSIENT,SQLite已经复制了数据,但为了代码清晰,我习惯在确认不再需要原始数据后再释放。
11.5 核心API:sqlite3_column_blob
读取BLOB数据,用的是sqlite3_column_blob。它返回一个const void*指针,指向SQLite内部存储的二进制数据。配合sqlite3_column_bytes获取数据长度:
const void *sqlite3_column_blob(sqlite3_stmt *stmt, int iCol);
int sqlite3_column_bytes(sqlite3_stmt *stmt, int iCol);
注意:返回的指针指向SQLite内部缓冲区,不要直接修改它。如果需要修改,先复制一份。
11.6 实战:从数据库读取并还原图片
读取刚才存进去的图片,写回磁盘:
// 查询
const char *sql = "SELECT avatar FROM user_avatar WHERE user_name = ?";
sqlite3_stmt *stmt;
sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
sqlite3_bind_text(stmt, 1, "张三", -1, SQLITE_STATIC);
// 执行查询
int rc = sqlite3_step(stmt);
if (rc == SQLITE_ROW) {
// 获取BLOB数据
const void *blob_data = sqlite3_column_blob(stmt, 0);
int blob_size = sqlite3_column_bytes(stmt, 0);
if (blob_data && blob_size > 0) {
// 写回文件
FILE *fp = fopen("restored_avatar.png", "wb");
if (fp) {
fwrite(blob_data, 1, blob_size, fp);
fclose(fp);
printf("图片还原成功!大小: %d 字节\n", blob_size);
}
}
} else {
printf("未找到记录\n");
}
sqlite3_finalize(stmt);
你看,读取比写入更简单。但有个坑:sqlite3_column_blob返回的数据只在sqlite3_step返回SQLITE_ROW的当前行有效。如果你调用了sqlite3_step获取下一行,或者调用了sqlite3_finalize,这个指针就失效了。所以,要么在行有效期内用完数据,要么立即复制一份。
11.7 避坑指南:我曾经踩过的雷
做BLOB处理时,有几个坑我印象特别深:
- 空BLOB vs NULL:存一个0字节的BLOB和存NULL是两回事。查询时用
IS NULL判断NULL,用length(avatar) = 0判断空BLOB。我有个项目就因为没区分,导致前端显示空白头像时逻辑混乱。 - 内存泄漏:用
SQLITE_STATIC时,SQLite不会复制数据。如果你在bind之后释放了原始数据,SQLite内部就变成野指针了。我曾经在循环中犯过这个错,查了两天才定位到。 - 大文件性能:超过10MB的文件,建议不要直接存BLOB。插入和查询都会变慢,而且数据库文件会迅速膨胀。我的经验是:头像、缩略图(几百KB以内)用BLOB,高清原图存文件系统。
- 编码问题:BLOB存的是原始字节,不涉及编码转换。但如果你把文本当BLOB存,读取时要注意编码一致性。我见过有人用BLOB存UTF-8文本,读取时当ASCII处理,结果中文全乱码。
11.8 知识体系一览
为了让你更直观地理解BLOB处理的整体流程,我画了一张图:
11.9 性能与最佳实践
关于BLOB的性能,我总结了几条经验:
- 小BLOB(< 1MB):直接存,性能影响可以忽略。我做过测试,100KB的图片,插入1000条也就多花几十毫秒。
- 中BLOB(1MB ~ 10MB):可以考虑压缩后再存。比如图片用JPEG格式,比PNG小很多。我有个项目把用户上传的图片统一转成JPEG 80%质量,体积缩小了5倍。
- 大BLOB(> 10MB):建议存文件路径。数据库只存元数据,文件放文件系统或对象存储。这是业界主流做法。
- 事务处理:批量插入BLOB时,记得用事务包裹。否则每插入一条就自动提交一次,性能惨不忍睹。
SELECT length(avatar) FROM user_avatar查看字节数。如果返回正数,说明数据存进去了。如果返回NULL,说明存的是NULL而不是BLOB。
printf("%s", blob_data)打印BLOB数据!二进制数据里可能包含'\0',会导致打印截断。更危险的是,如果包含不可打印字符,终端可能会乱码甚至崩溃。用hexdump或逐字节打印才是正道。
11.10 小结
BLOB处理其实不复杂,核心就是两个API:sqlite3_bind_blob写入,sqlite3_column_blob读取。但细节决定成败——内存管理、生命周期、性能取舍,这些才是真正体现功力的地方。
我个人建议,刚开始接触BLOB时,先从小文件练手。比如存几张几十KB的图片,反复读写几次,把流程跑通。等熟悉了再处理大文件。毕竟,数据库编程这东西,纸上谈兵不如动手一试。
公众号:蓝海资料掘金营,微信deep3321