2. SQLite核心API详解:从打开到关闭的完整旅程

说实话,SQLite的API设计得相当简洁。你想想看,一个完整的数据库操作,本质上就三件事:打开数据库、执行操作、关闭数据库。就这么简单。但简单归简单,里面有不少细节值得好好琢磨。

今天我要讲的这六个API,可以说是SQLite的"六脉神剑"。掌握了它们,你就能搞定90%以上的数据库操作。我个人习惯把这六个函数分成三组来理解:

  • 开与关:sqlite3_open / sqlite3_close
  • 快捷执行:sqlite3_exec
  • 精细控制:sqlite3_prepare_v2 / sqlite3_step / sqlite3_finalize

嗯,咱们一个一个来看。

SQLite 核心 API 体系 第一组:开与关 sqlite3_open sqlite3_close → 数据库生命周期管理 第二组:快捷执行 sqlite3_exec → 一步到位,适合 DDL / 简单操作 第三组:精细控制(推荐) 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。从那以后,我再也不敢跳过错误检查了。

💡 我的小技巧: 如果传入 ":memory:" 作为文件名,SQLite 会在内存中创建数据库。这在做单元测试或者临时数据处理时特别方便,速度飞快,而且用完即焚,不用操心清理。

关闭数据库就更简单了:

int sqlite3_close(sqlite3 *db);

但这里有个坑——如果还有未 finalize 的 prepared statement,sqlite3_close 会返回 SQLITE_BUSY,拒绝关闭。我曾经在写一个长连接服务时踩过这个坑,排查了半天才发现是某个查询忘了调用 sqlite3_finalize。

⚠️ 注意: 关闭前务必确保所有 prepared statement 都已 finalize。否则数据库句柄泄漏,内存也会泄漏。建议在 close 之后将 db 指针置为 NULL,防止野指针。

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 注入。

🔑 我的建议: sqlite3_exec 适合做数据库初始化、批量 DDL 操作。但对于需要处理查询结果、或者涉及用户输入的 SQL,请务必使用下面这组 API。

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);
🔑 核心要点: prepare → 循环 step(直到 DONE 或 ROW)→ finalize。这个模式一定要刻在脑子里。我在项目中见过太多人忘记 finalize,导致内存只增不减。

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 开始。这个区别我记错过好几次。
💡 我的习惯: 每次 prepare 之后,我都会立即检查返回值。如果失败,用 sqlite3_errmsg 打印错误信息。调试阶段这个习惯能帮你省下大量时间。

好了,这六个 API 就讲完了。说白了,sqlite3_exec 是"快餐",方便但粗糙;prepare/step/finalize 是"正餐",精细但需要多花点功夫。根据场景选对工具,才是工程师的智慧。


公众号:蓝海资料掘金营,微信deep3321