2. SQLite核心API详解:从打开到关闭的完整旅程
说实话,SQLite的API设计得相当简洁。你想想看,一个完整的数据库操作,本质上就三件事:打开数据库、执行操作、关闭数据库。就这么简单。但简单归简单,里面有不少细节值得好好琢磨。
今天我要讲的这六个API,可以说是SQLite的"六脉神剑"。掌握了它们,你就能搞定90%以上的数据库操作。我个人习惯把这六个函数分成三组来理解:
- 开与关:sqlite3_open / sqlite3_close
- 快捷执行:sqlite3_exec
- 精细控制:sqlite3_prepare_v2 / sqlite3_step / sqlite3_finalize
嗯,咱们一个一个来看。
2.1 开与关:sqlite3_open 和 sqlite3_close
先说说打开数据库。sqlite3_open 的原型长这样:
int sqlite3_open(
const char *filename, // 数据库文件路径
sqlite3 **ppDb // 输出参数,返回数据库句柄
);
返回值是整型,SQLITE_OK 表示成功,其他值表示出错。我刚开始用的时候,总觉得检查返回值是多余的——直到有一次在生产环境上,数据库文件所在的磁盘满了,open 直接返回了 SQLITE_CANTOPEN。从那以后,我再也不敢跳过错误检查了。
关闭数据库就更简单了:
int sqlite3_close(sqlite3 *db);
但这里有个坑——如果还有未 finalize 的 prepared statement,sqlite3_close 会返回 SQLITE_BUSY,拒绝关闭。我曾经在写一个长连接服务时踩过这个坑,排查了半天才发现是某个查询忘了调用 sqlite3_finalize。
2.2 快捷执行:sqlite3_exec
sqlite3_exec 是个"一站式"函数。你给它一个 SQL 字符串,它帮你编译、执行、收尾。原型如下:
int sqlite3_exec(
sqlite3 *db, // 数据库句柄
const char *sql, // 要执行的 SQL
sqlite3_callback callback, // 回调函数(可为 NULL)
void *arg, // 回调函数的第一个参数
char **errmsg // 错误信息输出
);
说白了,这就是个"懒人函数"。适合执行 CREATE TABLE、INSERT、UPDATE、DELETE 这类不需要逐行处理结果的语句。如果你需要读取查询结果,可以传一个回调函数,每查到一行数据就会调用一次。
举个例子:
sqlite3 *db;
char *err = NULL;
int rc = sqlite3_open("test.db", &db);
if (rc != SQLITE_OK) {
fprintf(stderr, "打开失败: %s\n", sqlite3_errmsg(db));
return -1;
}
const char *sql = "CREATE TABLE IF NOT EXISTS users(id INT, name TEXT);"
"INSERT INTO users VALUES(1, 'Alice');"
"INSERT INTO users VALUES(2, 'Bob');";
rc = sqlite3_exec(db, sql, NULL, NULL, &err);
if (rc != SQLITE_OK) {
fprintf(stderr, "执行失败: %s\n", err);
sqlite3_free(err);
}
sqlite3_close(db);
你看,多简洁。但简洁也有代价——你没法控制参数绑定,没法逐行处理结果。而且如果 SQL 字符串里有用户输入的内容,很容易引发 SQL 注入。
2.3 精细控制三剑客
这三兄弟才是 SQLite 的精髓。它们让你能像操作游标一样,逐行处理查询结果。
2.3.1 sqlite3_prepare_v2 — 编译 SQL
int sqlite3_prepare_v2(
sqlite3 *db, // 数据库句柄
const char *sql, // SQL 语句
int nByte, // SQL 长度(传 -1 表示自动计算)
sqlite3_stmt **ppStmt, // 输出:编译后的语句句柄
const char **pzTail // 指向未处理的 SQL 部分
);
这个函数把 SQL 文本编译成字节码。你想想看,SQL 是字符串,数据库要执行它,得先解析、语法分析、生成执行计划。prepare 做的就是这件事。
我个人习惯把 nByte 传 -1,让 SQLite 自己算长度。pzTail 参数很有意思——如果你传入了多条 SQL 语句,它会指向第一条之后的部分。你可以循环调用 prepare 来执行多条语句。
2.3.2 sqlite3_step — 执行一步
int sqlite3_step(sqlite3_stmt *stmt);
这个函数每次调用,就执行一步。对于 SELECT 查询,第一次调用会定位到第一行,返回 SQLITE_ROW。你可以用 sqlite3_column_xxx 系列函数读取当前行的各列数据。然后再次调用 step,移动到下一行。直到返回 SQLITE_DONE,表示没有更多数据了。
对于 INSERT、UPDATE、DELETE,第一次调用 step 就会执行完毕,返回 SQLITE_DONE。
2.3.3 sqlite3_finalize — 清理资源
int sqlite3_finalize(sqlite3_stmt *stmt);
这个函数释放 prepared statement 占用的所有资源。记住,每个 prepare 都必须对应一个 finalize,否则内存泄漏。
来看一个完整的例子:
sqlite3 *db;
sqlite3_stmt *stmt;
int rc;
sqlite3_open("test.db", &db);
// 准备查询
rc = sqlite3_prepare_v2(db, "SELECT id, name FROM users WHERE id > ?", -1, &stmt, NULL);
if (rc != SQLITE_OK) {
fprintf(stderr, "prepare 失败: %s\n", sqlite3_errmsg(db));
sqlite3_close(db);
return -1;
}
// 绑定参数(问号的位置从1开始)
sqlite3_bind_int(stmt, 1, 0);
// 逐行读取结果
while ((rc = sqlite3_step(stmt)) == SQLITE_ROW) {
int id = sqlite3_column_int(stmt, 0);
const char *name = (const char *)sqlite3_column_text(stmt, 1);
printf("id=%d, name=%s\n", id, name);
}
if (rc != SQLITE_DONE) {
fprintf(stderr, "step 出错: %s\n", sqlite3_errmsg(db));
}
// 清理
sqlite3_finalize(stmt);
sqlite3_close(db);
2.4 三组 API 的对比
| 特性 | sqlite3_exec | prepare/step/finalize |
|---|---|---|
| 使用复杂度 | 低,一行搞定 | 中,需要多步操作 |
| 参数绑定 | 不支持,需拼接 SQL | 支持,防 SQL 注入 |
| 逐行处理结果 | 通过回调,不够灵活 | 直接逐行,完全可控 |
| 性能(重复执行) | 每次重新编译 | 可复用 stmt,只编译一次 |
| 适用场景 | DDL、批量操作、简单脚本 | 查询、参数化操作、高性能场景 |
2.5 避坑指南
最后,分享几个我踩过的坑:
- 忘记检查返回值:每个 API 调用都要检查返回值。SQLITE_OK 不代表一切正常,SQLITE_ROW 和 SQLITE_DONE 也是正常状态。
- 多线程共用同一个 stmt:SQLite 的 stmt 不是线程安全的。多线程环境下,每个线程应该有自己的 stmt,或者加锁保护。
- sqlite3_column_text 返回的指针:这个指针指向的是 SQLite 内部缓冲区,在下次调用 sqlite3_step 或 sqlite3_finalize 后就会失效。如果你需要长期保存,记得用 strdup 复制一份。
- 参数索引从 1 开始:sqlite3_bind_xxx 的第二个参数是参数索引,从 1 开始。列索引从 0 开始。这个区别我记错过好几次。
好了,这六个 API 就讲完了。说白了,sqlite3_exec 是"快餐",方便但粗糙;prepare/step/finalize 是"正餐",精细但需要多花点功夫。根据场景选对工具,才是工程师的智慧。
公众号:蓝海资料掘金营,微信deep3321