10、预处理语句与参数绑定:sqlite3_prepare_v2详解、sqlite3_bind_*系列函数、sqlite3_column_*系列函数、防止SQL注入
说实话,很多刚接触SQLite的朋友,上来就是拼字符串构造SQL。我当年也这么干过,直到有一次线上数据被意外清空……嗯,那滋味不好受。今天咱们聊聊预处理语句,这玩意儿能让你写代码更安全、更高效。
为什么不用拼字符串?
你想想看,拼字符串最大的问题是什么?
- SQL注入风险:用户输入里带个
' OR 1=1 --,你的表就裸奔了 - 性能差:每次执行都要重新解析SQL,数据库引擎累得慌
- 类型处理麻烦:数字要转字符串,字符串要加引号,还得处理转义
我在项目中遇到过一位同事,他写了个用户登录模块,直接拼字符串。测试环境跑得好好的,上线第一天就被注入了。从那以后,我们团队立了规矩:任何拼接SQL的代码,代码审查直接打回。
sqlite3_prepare_v2:一切从这里开始
预处理语句的核心,就是先把SQL语句"编译"成一个内部对象。以后执行的时候,直接拿这个对象跑,不用再解析了。
sqlite3_stmt *stmt;
const char *sql = "INSERT INTO users (name, age) VALUES (?, ?);";
int rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
if (rc != SQLITE_OK) {
fprintf(stderr, "准备失败: %s\n", sqlite3_errmsg(db));
return rc;
}
这里有几个细节要注意:
- 第二个参数:SQL语句,用
?做占位符 - 第三个参数:传
-1表示自动计算长度,也可以传正数指定字节数 - 第四个参数:输出参数,拿到预处理语句句柄
- 第五个参数:一般传
NULL,表示不需要知道SQL末尾位置
我的习惯:我一般用
sqlite3_prepare_v2 而不是旧版的 sqlite3_prepare。v2版本会缓存schema信息,性能更好,而且能检测到表结构变化后自动重新编译。
sqlite3_bind_*:给占位符填值
占位符 ? 就像填空题的空格,sqlite3_bind_* 系列函数就是往空格里填答案。
| 函数 | 用途 | 示例 |
|---|---|---|
sqlite3_bind_int |
绑定整型 | sqlite3_bind_int(stmt, 1, 25); |
sqlite3_bind_double |
绑定浮点型 | sqlite3_bind_double(stmt, 1, 3.14); |
sqlite3_bind_text |
绑定字符串 | sqlite3_bind_text(stmt, 1, "张三", -1, SQLITE_TRANSIENT); |
sqlite3_bind_blob |
绑定二进制数据 | sqlite3_bind_blob(stmt, 1, data, len, SQLITE_STATIC); |
sqlite3_bind_null |
绑定NULL | sqlite3_bind_null(stmt, 1); |
第二个参数是占位符的索引,从1开始。第三个参数就是要绑定的值。
我曾经踩过的坑:
传
传
我建议:除非你非常确定内存生命周期,否则一律用
sqlite3_bind_text 的最后一个参数,传 SQLITE_TRANSIENT 还是 SQLITE_STATIC?传
SQLITE_TRANSIENT 表示SQLite会自己拷贝一份数据,你传完字符串后可以马上释放。传
SQLITE_STATIC 表示你保证字符串指针在SQLite使用期间一直有效。我建议:除非你非常确定内存生命周期,否则一律用
SQLITE_TRANSIENT。安全第一。
完整示例:插入一条用户记录
sqlite3 *db;
sqlite3_stmt *stmt;
// 打开数据库
sqlite3_open("test.db", &db);
// 准备语句
const char *sql = "INSERT INTO users (name, age, email) VALUES (?, ?, ?);";
sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
// 绑定参数
sqlite3_bind_text(stmt, 1, "李四", -1, SQLITE_TRANSIENT);
sqlite3_bind_int(stmt, 2, 30);
sqlite3_bind_text(stmt, 3, "lisi@example.com", -1, SQLITE_TRANSIENT);
// 执行
int rc = sqlite3_step(stmt);
if (rc == SQLITE_DONE) {
printf("插入成功!\n");
} else {
printf("插入失败: %s\n", sqlite3_errmsg(db));
}
// 重置,准备下一次使用
sqlite3_reset(stmt);
// 用完释放
sqlite3_finalize(stmt);
sqlite3_close(db);
注意看,sqlite3_step 执行完后,用 sqlite3_reset 重置,这样同一个 stmt 可以反复使用。批量插入时,这个技巧能省不少时间。
sqlite3_column_*:读取查询结果
查询数据时,用 sqlite3_column_* 系列函数从结果集中取值。
const char *sql = "SELECT id, name, age FROM users WHERE age > ?;";
sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
sqlite3_bind_int(stmt, 1, 18);
while (sqlite3_step(stmt) == SQLITE_ROW) {
int id = sqlite3_column_int(stmt, 0);
const unsigned char *name = sqlite3_column_text(stmt, 1);
int age = sqlite3_column_int(stmt, 2);
printf("ID: %d, 姓名: %s, 年龄: %d\n", id, name, age);
}
sqlite3_finalize(stmt);
列索引也是从0开始。常用的函数:
sqlite3_column_int— 取整型sqlite3_column_double— 取浮点型sqlite3_column_text— 取字符串(返回const unsigned char*)sqlite3_column_blob— 取二进制数据sqlite3_column_bytes— 取数据的字节数sqlite3_column_type— 取列的数据类型
小技巧:
sqlite3_column_type 返回 SQLITE_INTEGER、SQLITE_FLOAT、SQLITE_TEXT、SQLITE_BLOB、SQLITE_NULL 之一。如果你不确定数据库里存的是什么类型,先调这个函数判断一下,再取对应的值。我在处理用户自定义字段时经常这么干。
防止SQL注入:预处理语句就是最好的盾牌
说白了,SQL注入的本质就是:用户输入被当成了SQL代码执行。
预处理语句为什么能防注入?因为 ? 占位符绑定的值,永远被当作数据,而不是SQL代码。就算用户输入 ' OR 1=1 --,SQLite也会把它当成一个普通的字符串值,不会去解析它。
看个对比:
// ❌ 危险写法
char sql[256];
sprintf(sql, "SELECT * FROM users WHERE name = '%s'", user_input);
// 如果 user_input = "' OR 1=1 --",就变成了:
// SELECT * FROM users WHERE name = '' OR 1=1 --'
// 所有用户数据全暴露!
// ✅ 安全写法
const char *sql = "SELECT * FROM users WHERE name = ?;";
sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
sqlite3_bind_text(stmt, 1, user_input, -1, SQLITE_TRANSIENT);
// 不管 user_input 是什么,都只是 name 字段的值
注意:预处理语句只对
? 占位符绑定的值有效。如果你还是用 sprintf 拼表名、列名,那照样有注入风险。表名和列名不能用占位符,需要你自己做白名单校验。
知识体系总览
下面这张图,把整个流程串起来了:
几个实用建议
- 批量操作时重用stmt:绑定新值前调
sqlite3_reset,避免反复prepare,性能能提升好几倍 - 事务包裹批量写入:先
BEGIN TRANSACTION,批量执行完再COMMIT,速度飞起 - 检查返回值:每个SQLite API调用都要检查返回值,别偷懒。我见过太多因为没检查返回值,导致数据没写进去还浑然不知的案例
- 命名占位符:除了
?,还可以用?NNN(如?1、?2)、:AAA、@AAA、$AAA。我个人喜欢用?,简单直观
一句话总结:预处理语句 + 参数绑定 = 安全 + 高效。这是C语言操作SQLite的标配,别再用拼字符串那种原始方式了。
公众号:蓝海资料掘金营,微信deep3321