【消息队列项目】SQLite简单介绍

目录

[一.SQLite3 C/C++ API 介绍](#一.SQLite3 C/C++ API 介绍)

1.1.sqlite3_threadsafe()

1.2.sqlite3_open函数

1.3.sqlite3_open_v2函数

1.4.sqlite3_exec函数

1.5.sqlite3_close函数

1.6.sqlite3_close_v2函数

1.7.sqlite3_errmsg函数

二.封装SQLite3接口


SQLite 是一个遵循 ACID 特性的嵌入式关系型数据库引擎。

它的核心特点是作为一个进程内库运行,而不是一个独立的客户端-服务器数据库系统

这意味着 SQLite 的数据库引擎被直接链接到应用程序中,成为应用程序的一部分。整个数据库(包括定义、表、数据和索引)完全存储在一个独立的、跨平台的标准磁盘文件中。应用程序通过直接调用 SQLite 库的 API 来读写这个文件,无需任何中介的服务器进程。

由于其设计简洁、可靠且无需外部依赖,SQLite 已成为全球部署最广泛的数据库引擎,内嵌于无数的应用程序、操作系统和嵌入式设备中。

选择 SQLite 的主要原因:

  1. 无服务器架构 (Serverless)
  • 工作机制:与传统数据库(如 MySQL、PostgreSQL)不同,SQLite 没有单独的、需要启动和管理的数据库服务器进程。应用程序通过调用 SQLite 库(一个 .dll、.so 或 .dylib 文件)中的函数,直接与数据库文件进行交互。
  • 优势:这极大地简化了部署和维护。您只需分发您的应用程序和数据库文件,无需在用户环境中安装、配置或维护一个数据库服务。这也减少了系统资源的开销和潜在的复杂性。
  1. 零配置 (Zero-Configuration)
  • 具体表现:要开始使用 SQLite,您不需要进行任何"安装数据库系统"的操作。没有服务需要注册,没有配置文件需要编辑,也没有管理员账户需要创建。在代码中,您只需要指定一个文件路径(例如 myapp.db),如果文件不存在,SQLite 会自动创建它。
  • 优势:这使得开发、测试和部署变得极其简单快捷,尤其适合原型开发、单机应用和嵌入式系统。
  1. 单一文件数据库 (Single-File Database)
  • 存储方式:SQLite 将整个数据库(表结构、数据、索引、触发器、视图等,以及用于保障事务安全的日志信息)存储在一个普通的操作系统文件中。这个文件可以放在任何目录下,并像其他文件一样被复制、移动或备份。
  • 优势:便携性极佳。迁移数据库只需拷贝一个文件。备份也简化为了文件备份。这种透明性让开发者能直观地理解和管理数据存储。
  1. 轻量级与紧凑性 (Lightweight & Compact)
  • 量化说明:SQLite 的核心库代码体积非常小。在完整编译所有可选功能的情况下,其大小通常小于 500KB;如果仅启用核心功能,库文件可以缩小到 300KB 以下。运行时内存占用也较少。
  • 优势:这种轻量级特性使其非常适合资源受限的环境,如移动应用(iOS/Android)、嵌入式设备(IoT)、小型桌面工具,或作为大型应用程序的本地缓存存储。
  1. 自给自足与无依赖性 (Self-Contained)
  • 技术细节:SQLite 使用 ANSI-C 编写,其源代码不依赖于任何外部库(除标准 C 库外)。整个数据库引擎的逻辑,包括 SQL 编译器、虚拟机、B-tree 存储引擎和页面缓存,都包含在一个库文件中。
  • 优势:确保了高度的可移植性和可靠性。您可以在几乎任何平台上编译和运行 SQLite,无需担心系统缺少特定的依赖项。
  1. 完整的 ACID 事务 (Fully ACID-Compliant Transactions)
  • ACID 详解:
  • 原子性:一个事务中的所有操作要么全部完成,要么全部不生效,即使系统发生崩溃或断电。
  • 一致性:事务确保数据库从一个一致的状态转换到另一个一致的状态。
  • 隔离性:SQLite 默认使用 串行化 隔离级别,这意味着在某一时刻,只有一个写操作可以访问数据库文件,从而确保了最高级别的数据一致性。它也支持通过"预写日志"模式实现更高级的并发读/写。
  • 持久性:一旦事务提交,其对数据的修改就是永久性的,即使后续发生系统故障。
  • 优势:这保证了数据的完整性和可靠性,即使在多线程或多进程环境下,也能安全地进行数据操作。
  1. 广泛的 SQL 支持 (Broad SQL Support)
  • 能力范围:虽然名为 "SQLite",但它支持绝大部分 SQL-92 标准的核心特性,包括:
  • 复杂的 SELECT 查询(含连接、子查询、聚合函数)。
  • CREATE, ALTER, DROP 表与索引。
  • INSERT, UPDATE, DELETE 数据操作。
  • 触发器(Trigger)和视图(View)。
  • 事务控制 (BEGIN, COMMIT, ROLLBACK)。
  • 它支持大多数常见的数据类型(如 INTEGER, REAL, TEXT, BLOB, NULL)。
  1. 易于使用的 API (Simple API)
  • 接口设计:SQLite 提供的 C语言 API 清晰且直观,通常只需几个核心函数(如 sqlite3_open, sqlite3_exec, sqlite3_prepare_v2, sqlite3_step, sqlite3_close)即可完成大部分工作。围绕这个核心 C API,社区为几乎所有编程语言(如 Python, Java, C#, PHP, JavaScript 等)提供了成熟、易用的封装接口。
  • 优势:学习曲线平缓,开发者可以快速上手并集成到项目中。
  1. 卓越的跨平台性 (Cross-Platform)
  • 运行环境:SQLite 几乎可以在所有现代操作系统上运行,包括 Windows, Linux, macOS, Android, iOS,以及各种 Unix 变体和嵌入式实时操作系统。数据库文件本身在不同平台间也是兼容的。
  • 优势:使用 SQLite 开发的应用可以无缝地跨平台部署,数据文件可以在不同系统间直接交换使用。

典型应用场景

  • 移动应用:作为本地数据存储(通讯录、聊天记录、应用配置)。
  • 嵌入式系统与物联网设备:存储设备配置、日志和采集的数据。
  • 桌面软件:存储用户数据、历史记录和缓存。
  • 网站:作为低流量小型网站的数据库,或用作应用内的本地缓存数据库。
  • 数据分析与数据处理:作为中间格式,方便地导入、导出和查询数据文件。
  • 应用程序文件格式:许多应用程序使用 SQLite 数据库文件作为其私有文档格式(如 .db, .sqlite 文件)。

需注意的局限性

SQLite 并非适用于所有场景,以下情况不适合使用 SQLite:

  • 高并发写操作:当存在大量需要同时写入数据库的客户端时(如大型网站后端),其文件锁机制可能成为瓶颈。
  • 超大规模数据集:虽然理论上支持 TB 级数据库,但单个文件存储所有数据在管理和备份上可能变得笨拙。
  • 需要网络访问的数据库:SQLite 是一个本地文件数据库,不原生支持通过网络协议进行远程访问。客户端-服务器数据库(如 PostgreSQL)在这方面是更佳选择。
  • 需要高级数据库功能:如存储过程、自定义函数、复杂的用户权限管理系统等。

一.SQLite3 C/C++ API 介绍

C/C++ API是SQLite3数据库的一个客户端, 提供一种用C/C++操作数据库的方法。

我们可以去官网看看:List Of SQLite Functions

1.1.sqlite3_threadsafe()

函数定义与作用

复制代码
int sqlite3_threadsafe(void);

这是一个运行时查询函数,用于在程序运行时检查当前链接的 SQLite 库在编译阶段是否启用了线程安全特性。

返回值说明

  • 返回 0:表示 SQLite 库编译时完全禁用了所有线程安全机制
  • 返回 1 或 2:表示 SQLite 库编译时启用了线程安全支持

二、SQLite 的三种线程安全模式详解

  1. 模式一:单线程模式(非线程安全)
  • 在这种模式下,SQLite 库完全不包含任何与线程同步相关的代码
  • 所有内部的互斥锁(mutex)、临界区(critical section)等同步原语都被排除在编译之外
  • 数据库连接句柄(sqlite3*)绝对不能在多个线程间共享

典型应用场景:

  • 单线程应用程序
  • 嵌入式设备中确定只有一个执行线程
  • 对性能要求极高且可保证单线程访问的场景
  1. 模式二:多线程模式(线程安全,连接隔离)

技术细节:

  • SQLite 库包含完整的线程同步代码
  • 核心规则:一个数据库连接(sqlite3* 句柄)只能由创建它的线程使用
  • 不同线程可以同时操作不同的数据库连接
  • SQLite 内部使用互斥锁保护共享数据结构

线程使用规则表:

操作 是否允许 说明
线程A创建连接,线程A使用 ✅ 允许 正常使用
线程A创建连接,线程B使用 ❌ 禁止 违反规则
线程A创建连接A,线程B创建连接B ✅ 允许 各自使用自己的连接
  1. 模式三:串行化模式(完全线程安全)

技术细节:

  • 这是最严格的线程安全模式
  • 允许同一个数据库连接在多个线程间共享
  • SQLite 内部使用更细粒度的锁机制,确保所有操作串行执行
  • 性能相比模式二有所降低,因为需要更多的锁操作

关键特性:

  • 多个线程可以安全地使用同一个 sqlite3* 句柄
  • SQLite 保证所有数据库操作是原子的、顺序执行的
  • 应用程序不需要额外的同步机制来保护 SQLite 调用

1.2.sqlite3_open函数

函数原型:

复制代码
int sqlite3_open(
  const char *filename,   /* 数据库文件名(UTF-8) */
  sqlite3 **ppDb          /* 输出参数:返回的数据库连接句柄 */
);

功能:

这个函数用于打开一个SQLite数据库文件,并返回一个数据库连接句柄。如果指定的文件不存在,且没有设置其他限制,SQLite会创建一个新的数据库文件。

参数说明:

  1. filename :数据库文件的路径。如果文件名是**:memory:** ,则表示在内存中创建一个临时数据库(只在当前连接的生命周期内存在)。如果文件名是空字符串"",则会创建一个临时的磁盘数据库(当连接关闭时会被删除)。

  2. ppDb :一个指向sqlite3*指针的指针。这个是一个输出型参数函数将把打开的数据库连接的句柄写入这个指针。

返回值:

  • 返回SQLITE_OK(即0)表示成功。

  • 如果发生错误,则返回一个错误代码(非零值),此时*ppDb可能会被设置为一个错误句柄,可以通过sqlite3_errmsg()函数获取错误描述。

工作方式:

  1. 当调用sqlite3_open时,SQLite会尝试打开或创建指定的数据库文件。

  2. 如果文件路径不存在,且目录可写,SQLite会创建一个新的数据库文件。

  3. 如果文件路径存在,SQLite会尝试打开它,并验证它是否是一个有效的SQLite数据库文件(除非使用了其他选项,如SQLITE_OPEN_CREATE,否则不会自动创建)。

注意:

  • 如果文件名是NULL,则行为是未定义的。

  • 即使打开失败,也应该调用sqlite3_close来关闭句柄,以释放资源。

示例代码:

cpp 复制代码
#include <stdio.h>
#include <sqlite3.h>

int main() {
    sqlite3 *db;
    int rc;

    rc = sqlite3_open("test.db", &db);
    if (rc != SQLITE_OK) {
        fprintf(stderr, "无法打开数据库: %s\n", sqlite3_errmsg(db));
        sqlite3_close(db);
        return 1;
    }

    printf("数据库打开成功\n");

    // ... 使用数据库进行各种操作 ...

    sqlite3_close(db);
    return 0;
}

然后我们直接编译运行

非常的完美

1.3.sqlite3_open_v2函数

函数原型:

cpp 复制代码
int sqlite3_open_v2(
  const char *filename,   /* 数据库文件名(UTF-8) */
  sqlite3 **ppDb,         /* 输出参数:返回的数据库连接句柄 */
  int flags,              /* 标志位,控制打开行为 */
  const char *zVfs        /* 使用的VFS模块的名称,如果为NULL则使用默认 */
);

功能:

这个函数是sqlite3_open的增强版本,允许通过flags参数指定更多的打开选项,并且可以选择使用特定的VFS(虚拟文件系统)模块。

参数说明:

  1. filename :数据库文件的路径。如果文件名是**:memory:** ,则表示在内存中创建一个临时数据库(只在当前连接的生命周期内存在)。如果文件名是空字符串"",则会创建一个临时的磁盘数据库(当连接关闭时会被删除)。

  2. ppDb :一个指向sqlite3*指针的指针。这个是一个输出型参数函数将把打开的数据库连接的句柄写入这个指针。

  3. flags :一个整数,由多个标志位通过按位或(|)组合而成,用于控制数据库的打开方式。常用的标志位包括:

    • SQLITE_OPEN_READONLY:以只读方式打开数据库。

    • SQLITE_OPEN_READWRITE:以可读可写方式打开数据库,但如果文件不存在,不会自动创建。

    • SQLITE_OPEN_CREATE:如果数据库文件不存在,则创建它。必须与SQLITE_OPEN_READWRITE一起使用。

    • SQLITE_OPEN_NOMUTEX:**以多线程模式打开数据库连接,即连接句柄不能在多个线程间共享(每个线程必须使用自己的连接)。**但是,如果多个线程使用不同的连接,它们可以同时访问数据库。

    • SQLITE_OPEN_FULLMUTEX以串行化模式打开数据库连接,允许连接句柄在多个线程间共享(但需要应用程序自己管理线程同步,实际上SQLite会保证同一时间只有一个线程使用该连接执行操作)。

    • 还有其他一些标志,如SQLITE_OPEN_URISQLITE_OPEN_MEMORY等,但以上是最常用的。

  4. zVfs:指定要使用的VFS模块的名称。如果为NULL,则使用默认的VFS。

返回值:

  • 返回SQLITE_OK(即0)表示成功。

  • 如果发生错误,则返回一个错误代码(非零值),此时*ppDb可能会被设置为一个错误句柄,可以通过sqlite3_errmsg()函数获取错误描述。

工作方式:

  1. 根据flags参数的要求打开或创建数据库。

  2. 如果flags中指定了SQLITE_OPEN_CREATE,则当文件不存在时会创建新文件;否则,文件必须存在,否则打开失败。

  3. 线程安全模式的选择:

    • 使用SQLITE_OPEN_NOMUTEX:打开的连接句柄不能在多个线程间共享,但不同的连接可以在不同的线程中同时使用(前提是SQLite编译时启用了线程安全)。

    • 使用SQLITE_OPEN_FULLMUTEX:打开的连接句柄可以在多个线程间共享,但同一时间只能有一个线程使用该连接执行操作。

注意:

  • 如果同时指定了SQLITE_OPEN_READWRITESQLITE_OPEN_CREATE,则当文件不存在时会创建新文件,如果文件存在则正常打开。

  • 如果只指定了SQLITE_OPEN_READWRITE,而文件不存在,则打开失败。

  • 线程安全模式的选择需要根据应用程序的需求。如果每个线程使用独立的连接,那么使用SQLITE_OPEN_NOMUTEX(多线程模式)可以获得更好的性能。如果需要在多个线程间共享同一个连接,则必须使用SQLITE_OPEN_FULLMUTEX(串行化模式)。

示例代码:

示例1:以可读可写方式打开,如果不存在则创建

cpp 复制代码
#include <stdio.h>
#include <sqlite3.h>

int main() {
    sqlite3 *db;
    int flags = SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE;
    int rc = sqlite3_open_v2("test.db", &db, flags, NULL);
    if (rc != SQLITE_OK) {
        fprintf(stderr, "无法打开数据库: %s\n", sqlite3_errmsg(db));
        sqlite3_close(db);
        return 1;
    }

    printf("数据库打开成功\n");

    // ... 使用数据库进行各种操作 ...

    sqlite3_close(db);
    return 0;
}

示例2:以只读方式打开

cpp 复制代码
sqlite3 *db;
int flags = SQLITE_OPEN_READONLY;
int rc = sqlite3_open_v2("test.db", &db, flags, NULL);
if (rc != SQLITE_OK) {
    // 错误处理
}

示例3:以多线程模式打开(连接不能跨线程共享)

cpp 复制代码
sqlite3 *db;
int flags = SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE | SQLITE_OPEN_NOMUTEX;
int rc = sqlite3_open_v2("test.db", &db, flags, NULL);
if (rc != SQLITE_OK) {
    // 错误处理
}

示例4:以串行化模式打开(连接可以跨线程共享,但需要串行使用)

cpp 复制代码
sqlite3 *db;
int flags = SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE | SQLITE_OPEN_FULLMUTEX;
int rc = sqlite3_open_v2("test.db", &db, flags, NULL);
if (rc != SQLITE_OK) {
    // 错误处理
}

三、关于线程安全模式的补充说明

  1. 多线程模式(SQLITE_OPEN_NOMUTEX

    • 连接句柄只能由打开它的线程使用。

    • 不同线程可以使用不同的连接句柄同时访问同一个数据库文件。

    • 这种模式下,SQLite内部会使用较少的锁,因此性能更高。

  2. 串行化模式(SQLITE_OPEN_FULLMUTEX

    • 连接句柄可以在多个线程间传递,但同一时间只能有一个线程使用该连接执行操作。

    • 这种模式下,SQLite内部会使用更多的锁来确保线程安全,因此性能稍低。

1.4.sqlite3_exec函数

sqlite3_exec 是 SQLite 库中用于执行 SQL 语句的函数。

它特别适合于执行不需要返回数据的 SQL 语句(如 INSERT、UPDATE、DELETE),但也可以用于执行查询语句(SELECT)并处理结果。

函数原型:

cpp 复制代码
int sqlite3_exec(
  sqlite3* db,                               /* 一个打开的数据库句柄 */
  const char *sql,                           /* 要执行的SQL语句 */
  int (*callback)(void*, int, char**, char**), /* 回调函数,用于处理查询结果 */
  void *arg,                                 /* 传递给回调函数的第一个参数 */
  char **errmsg                              /* 错误信息指针的地址 */
);

参数详解

sqlite3* db

  • 这是一个已经打开的数据库连接句柄。它来自于之前调用 sqlite3_open 或 sqlite3_open_v2 获取的。所有操作都将在这个数据库连接上执行。

const char *sql

  • 这是一个字符串,包含要执行的一条或多条SQL语句。多条语句之间可以用分号(;)分隔。注意,虽然可以执行多条,但一般不推荐,因为这样容易受到SQL注入攻击(如果SQL来自用户输入)。而且,如果其中一条语句失败,则后面的语句不会执行。

int (*callback)(void*, int, char**, char**)

这是一个函数指针,指向一个回调函数。**当执行SELECT等返回数据的SQL语句时,sqlite3_exec会为结果集中的每一行调用一次这个回调函数。**如果你执行的不是返回数据的语句(如INSERT, UPDATE, DELETE, CREATE等),则不会调用回调函数,可以传入NULL。

注意这个回调函数是系统在调用,不是我们手工调用的!!!

回调函数的参数和返回值如下:

  • 第一个参数:由sqlite3_exec的第四个参数(void *arg)传递过来,你可以用它来传递任何数据给回调函数。
  • 第二个参数:表示这一行有多少个列(字段)。
  • 第三个参数:是一个字符串数组,表示这一行每一列的数据(以字符串形式,即使列类型不是字符串)。
  • 第四个参数:是一个字符串数组,表示每一列的列名(字段名)。
  • 返回值:回调函数应该返回0。如果返回非零,sqlite3_exec将中断执行并返回SQLITE_ABORT。

void *arg

  1. 这个参数会作为回调函数的第一个参数传递。你可以用它来传递任意数据给回调函数,比如一个结构体指针,这样在回调函数内部就可以访问到这个结构体。如果不需要传递,可以传入NULL。

char **errmsg

  • 这是一个指向字符指针的指针,用于获取错误信息。如果执行过程中发生错误,sqlite3_exec会分配一段内存来存储错误描述字符串(通过sqlite3_malloc),并将错误字符串的地址赋值给*errmsg。因此,调用者需要负责使用sqlite3_free来释放这段内存。如果没有错误,*errmsg会被设置为NULL。

返回值

  • 函数返回一个整数,表示执行的状态。如果成功,返回SQLITE_OK(即0);如果出现错误,则返回对应的错误代码(如SQLITE_ERROR等)。

话不多说,我们直接看例子

不会调用回调函数的实例

如果你执行的不是返回数据的语句(如INSERT, UPDATE, DELETE, CREATE等),则系统不会调用回调函数,可以传入NULL。

实例1:最简单的创建表操作

cpp 复制代码
#include <stdio.h>
#include <sqlite3.h>

int main() {
    sqlite3 *db;
    char *errmsg = NULL;
    
    // 打开或创建数据库
    if(sqlite3_open("my.db", &db) != SQLITE_OK) {
        printf("打开数据库失败\n");
        return -1;
    }
    
    // 创建表(不需要回调函数,所以设为NULL)
    const char *sql = "CREATE TABLE IF NOT EXISTS student(id INTEGER PRIMARY KEY, name TEXT, score REAL);";
    
    if(sqlite3_exec(db, sql, NULL, NULL, &errmsg) != SQLITE_OK) {
        printf("SQL错误: %s\n", errmsg);
        sqlite3_free(errmsg);
    } else {
        printf("创建表成功!\n");
    }
    
    sqlite3_close(db);
    return 0;
}

实例2:插入数据

cpp 复制代码
#include <stdio.h>
#include <sqlite3.h>

int main() {
    sqlite3 *db;
    char *errmsg = NULL;
    
    sqlite3_open("my.db", &db);
    
    // 插入一条记录
    const char *sql = "INSERT INTO student(name, score) VALUES('小明', 85.5);";
    
    if(sqlite3_exec(db, sql, NULL, NULL, &errmsg) != SQLITE_OK) {
        printf("插入失败: %s\n", errmsg);
        sqlite3_free(errmsg);
    } else {
        printf("插入成功!\n");
    }
    
    sqlite3_close(db);
    return 0;
}

实例3:批量执行多条SQL语句

cpp 复制代码
#include <stdio.h>
#include <sqlite3.h>

int main() {
    sqlite3 *db;
    char *errmsg = NULL;
    
    sqlite3_open("my.db", &db);
    
    // 多条SQL语句,用分号分隔
    const char *sql = 
        "INSERT INTO student(name, score) VALUES('小红', 92.0);"
        "INSERT INTO student(name, score) VALUES('小刚', 78.5);"
        "UPDATE student SET score = 88.0 WHERE name = '小明';";
    
    if(sqlite3_exec(db, sql, NULL, NULL, &errmsg) != SQLITE_OK) {
        printf("执行失败: %s\n", errmsg);
        sqlite3_free(errmsg);
    } else {
        printf("批量执行成功!\n");
    }
    
    sqlite3_close(db);
    return 0;
}

会调用回调函数的实例

当执行SELECT等返回数据的SQL语句时,sqlite3_exec会为结果集中的每一行调用一次这个回调函数。

首先我们要明白,这个sqlite3_exec()只有第4个参数是作为回调函数第一个参数的

回调函数也不是我们手动去调用的,而是系统在调用的!!

那么回调函数其余参数都是系统自己在填充的!!!我们直接拿来使用即可

实例1:简单查询(使用回调)

cpp 复制代码
#include <stdio.h>
#include <sqlite3.h>

// 最简单的回调函数
int my_callback(void *data, int argc, char **argv, char **col_names) {
    // argc: 列数
    // argv: 每列的值
    // col_names: 每列的字段名
    for(int i = 0; i < argc; i++) {
        printf("%s: %s  ", col_names[i], argv[i] ? argv[i] : "NULL");
    }
    printf("\n");
    
    return 0;  // 必须返回0
}

int main() {
    sqlite3 *db;
    char *errmsg = NULL;
    
    sqlite3_open("my.db", &db);
    
    // 查询所有学生
    const char *sql = "SELECT * FROM student;";
    
    printf("查询结果:\n");
    printf("==========\n");
    
    if(sqlite3_exec(db, sql, my_callback, NULL, &errmsg) != SQLITE_OK) {
        printf("查询失败: %s\n", errmsg);
        sqlite3_free(errmsg);
    }
    
    sqlite3_close(db);
    return 0;
}

实例2:使用arg参数传递数据

cpp 复制代码
#include <stdio.h>
#include <sqlite3.h>

int callback(void *data, int argc, char **argv, char **col_names) {
    int *count = (int*)data;  // 传入的计数变量
    
    printf("第%d条记录: ", (*count) + 1);
    for(int i = 0; i < argc; i++) {
        printf("%s=%s ", col_names[i], argv[i]);
    }
    printf("\n");
    
    (*count)++;  // 记录数加1
    return 0;
}

int main() {
    sqlite3 *db;
    char *errmsg = NULL;
    int record_count = 0;  // 用于统计记录数
    
    sqlite3_open("my.db", &db);
    
    // 查询并统计记录数
    const char *sql = "SELECT id, name, score FROM student;";
    
    printf("学生列表:\n");
    
    sqlite3_exec(db, sql, callback, &record_count, &errmsg);
    
    printf("\n共有 %d 条记录\n", record_count);
    
    sqlite3_close(db);
    return 0;
}

可以看到我们调用sqlite3_exec的时候传递了一个&record_count给回调函数


实例3:错误处理的完整示例

cpp 复制代码
#include <stdio.h>
#include <sqlite3.h>

int callback(void *data, int argc, char **argv, char **col_names) {
    for(int i = 0; i < argc; i++) {
        printf("%-10s", argv[i] ? argv[i] : "NULL");
    }
    printf("\n");
    return 0;
}

int main() {
    sqlite3 *db;
    char *errmsg = NULL;
    int rc;  // 返回码
    
    // 1. 打开数据库
    rc = sqlite3_open("test.db", &db);
    if(rc != SQLITE_OK) {
        printf("无法打开数据库: %s\n", sqlite3_errmsg(db));
        return rc;
    }
    
    // 2. 创建表
    rc = sqlite3_exec(db, 
        "CREATE TABLE IF NOT EXISTS users(id INTEGER PRIMARY KEY, name TEXT);", 
        NULL, NULL, &errmsg);
    if(rc != SQLITE_OK) {
        printf("创建表错误: %s\n", errmsg);
        sqlite3_free(errmsg);
    }
    
    // 3. 插入数据
    rc = sqlite3_exec(db, 
        "INSERT INTO users(name) VALUES('张三'), ('李四'), ('王五');", 
        NULL, NULL, &errmsg);
    if(rc != SQLITE_OK) {
        printf("插入错误: %s\n", errmsg);
        sqlite3_free(errmsg);
    }
    
    // 4. 查询数据
    printf("ID        NAME\n");
    printf("----------------\n");
    rc = sqlite3_exec(db, "SELECT * FROM users;", callback, NULL, &errmsg);
    if(rc != SQLITE_OK) {
        printf("查询错误: %s\n", errmsg);
        sqlite3_free(errmsg);
    }
    
    // 5. 关闭数据库
    sqlite3_close(db);
    return 0;
}

1.5.sqlite3_close函数

第一部分:核心概念 ------ "句柄"是什么?

sqlite3* db 这个参数,我们叫它"数据库句柄"或"连接句柄"。

它就像你从图书馆管理员那里拿到的一张"借书卡"或者"座位号"。

当你调用 sqlite3_open(打开数据库)时,就相当于图书馆给你分配了一个专属的座位(db),并把座位号(句柄)交给你。

之后,你在图书馆里的所有操作(找书、看书、做笔记),都必须通过这个座位号来进行。系统知道你在这个座位上做什么。

这个句柄 db 不是数据库文件本身,而是你当前和数据库之间的一个 活动连接、一个会话通道。

第二部分:销毁句柄 ------ 把"借书卡"还回去

书看完了,你得把卡还回去,腾出座位。在SQLite里,就是关闭连接,销毁句柄。

cpp 复制代码
int sqlite3_close(sqlite3* db);

这是什么? 这是最基本的关闭函数。

它怎么做?

  • 它检查你这个"座位"(句柄db)上还有没有没做完的事(例如:有没有还没执行完的SQL语句,有没有没取完的查询结果)。
  • 如果有未完成的事,它会直接拒绝还卡,返回一个错误码(不是SQLITE_OK)。 这时连接并不会关闭。
  • 如果所有事都做完了,它就帮你清理座位,销毁句柄,然后说"好了",返回 SQLITE_OK。

缺点: 如果程序员不小心忘了做完某些事,sqlite3_close 可能会失败,但句柄又处于一个"半关闭"的奇怪状态,后续不好处理。

1.6.sqlite3_close_v2函数

其实这个sqlite3_close其实是有缺陷的,那么官方就对他进行了升级,也就是我们下面这个函数

cpp 复制代码
int sqlite3_close_v2(sqlite3* db); (推荐使用这个)

这是什么? 这是升级版的、更健壮的关闭函数。

它怎么做?

  • 它无论如何都会先把你"借书卡"(句柄db)的权限标记为失效。从这一刻起,你再也不能用这张卡做任何操作了。
  • 然后,它再去后台慢慢检查、清理你座位上留下的东西(未完成的任务)。
  • 最后,它总是会返回 SQLITE_OK,告诉你"关闭流程已成功启动"。 至于清理工作,库会自己在后台妥善完成。

为什么推荐它? 因为它行为更可预测、更安全。你不会因为忘记处理某个细节而导致关闭函数"卡住"或失败。对于初学者来说,用 _v2 版本更省心。

简单比喻:

  • sqlite3_close:管理员说:"把你桌上的书都还了,笔帽都盖好,我才收卡。" 如果你没做好,他就不收。
  • sqlite3_close_v2:管理员二话不说,先把你的卡收走注销,然后说"你可以走了,剩下收拾桌子的活儿我来"。他总是会说"好的,卡已收回"。

所以,你的代码通常会这样写:

cpp 复制代码
sqlite3 *db; // 声明一个"借书卡"
// ... (这里用 sqlite3_open 拿到"卡"db,并进行各种数据库操作) ...

// 最后,用推荐的方法还卡
int rc = sqlite3_close_v2(db); // rc 返回值,这里总是 SQLITE_OK
// 关闭后,db 指针就不要再使用了!

1.7.sqlite3_errmsg函数

获取错误信息 ------ 问问"刚才为什么出错?"

cpp 复制代码
const char *sqlite3_errmsg(sqlite3* db);

这是什么? 这是一个 "问询函数"。

它怎么用?

  • 当你进行某个操作(比如打开数据库、执行某条SQL语句)失败后,你需要知道具体错在哪里。
  • 这时,你就拿着你的"借书卡"(句柄db),去问管理员(调用这个函数)。
  • 管理员(函数)会根据你这张卡对应的、最近一次的操作记录,告诉你一个用文字描述的、人类可读的错误原因。

关键点:

  • 它问的是"针对这个连接(db)的最后一次错误"。 错误信息是和你的句柄 db 绑定的。
  • 它返回的是一个字符串(const char *),比如 "no such table: students"(没有叫students的表),这比一个单纯的错误数字代码好理解得多。
  • 一定要在出错后、进行下一个可能覆盖错误的操作前,赶紧调用它来问。

使用例子:

cpp 复制代码
sqlite3 *db;
// 尝试打开数据库
if (sqlite3_open("test.db", &db) != SQLITE_OK) {
    // 打开失败了!立刻用对应的db句柄去问错误信息
    printf("打开数据库失败:%s\n", sqlite3_errmsg(db));
    // 这里不应该调用 close,因为打开失败时,db 可能没有被正确分配
    return;
}

// 尝试执行一条错误的SQL
char *errMsg = NULL;
if (sqlite3_exec(db, "SELECT * FROM a_table_not_exist", NULL, NULL, &errMsg) != SQLITE_OK) {
    // 执行失败了!
    printf("执行SQL失败:%s\n", sqlite3_errmsg(db)); // 方式一:通过句柄问
    // 或者,sqlite3_exec 已经把错误信息填到 errMsg 里了
    printf("执行SQL失败(另一种方式):%s\n", errMsg); // 方式二
    sqlite3_free(errMsg); // 记得释放 sqlite3_exec 给的这个错误消息内存
}

// ... 最后,正常关闭 ...
sqlite3_close_v2(db);

二.封装SQLite3接口

在实际运用中,我们还是将上述接口全部封装到一个类里面

sqlite.h

cpp 复制代码
#include <sqlite3.h>
#include<iostream>
#include<string>

/**
 * SQLite 数据库帮助类
 * 封装了常用的 SQLite 操作,简化数据库使用
 */
class SqliteHelper { 
public: 
    // 定义回调函数类型别名,与 sqlite3_exec 的回调函数签名一致
    typedef int (*sqlite_callback)(void*,int,char**,char**); 
    
    SqliteHelper(const std::string&filename):_db_handler(nullptr),_dbfile(filename){} 
    
    ~SqliteHelper(){ close(); } 
    
    
    bool open(int thread_safe_level = SQLITE_OPEN_FULLMUTEX) { 
        // 设置打开标志------读写模式、不存在时创建、线程安全级别
        int flag = SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE | thread_safe_level; 

        // 使用带标志的open函数打开数据库
        int ret=sqlite3_open_v2(_dbfile.c_str(), &_db_handler, flag, nullptr);
        // 检查数据库打开是否成功
        if (ret != SQLITE_OK) { 
            // 输出错误信息
            std::cout << "打开数据库文件 " << _dbfile << " 失败!\n"; 
            std::cout << sqlite3_errmsg(_db_handler) << std::endl; 
            
            // 关闭失败的连接
            sqlite3_close(_db_handler); 
            return false; 
        } 
        return true; 
    } 
    
    void close() { 
        if (_db_handler) sqlite3_close(_db_handler); 
        _db_handler = nullptr; 
    } 
    
    bool exec(const std::string &sql, sqlite_callback cb = nullptr, void *arg = nullptr) { 
        // 执行SQL语句
        int ret = sqlite3_exec(_db_handler, sql.c_str(), cb, arg, nullptr); 
        // 检查执行结果
        if (ret != SQLITE_OK) { 
            std::cout << "执行 sql: " << sql << " 失败!\n"; 
            std::cout << "原因: " << sqlite3_errmsg(_db_handler) << std::endl; 
            return false; 
        } 
        return true; 
    } 
    
private: 
    sqlite3 *_db_handler;  // SQLite 数据库句柄,管理数据库连接
    std::string _dbfile;   // 数据库文件路径
};

然后我们写一个main.cpp来测试

cpp 复制代码
#include"sqlite.h"

#include <iostream>
#include <string>

int callback(void* data, int argc, char** argv, char** azColName) {
    // 将传入的数据转换为字符串,作为输出前缀
    std::string* prefix = static_cast<std::string*>(data);
    
    for (int i = 0; i < argc; i++) {
        // 打印列名和对应的值
        std::cout << *prefix << azColName[i] << " = ";
        
        // 检查值是否为NULL
        if (argv[i]) {
            std::cout << argv[i];
        } else {
            std::cout << "NULL";
        }
        std::cout << std::endl;
    }
    std::cout << "------------------------" << std::endl;
    
    return 0;
}

int main() {
    std::cout << "=== SQLite 数据库测试程序 ===" << std::endl;
    
    // 1. 创建SqliteHelper对象,指定数据库文件
    SqliteHelper dbHelper("test.db");
    
    std::cout << "1. 尝试打开数据库..." << std::endl;
    
    // 2. 打开数据库连接
    if (!dbHelper.open()) {
        std::cout << "数据库打开失败,程序退出。" << std::endl;
        return 1;
    }
    std::cout << "数据库打开成功!" << std::endl;
    
    // 3. 创建测试表
    std::cout << "\n2. 创建测试表..." << std::endl;
    std::string createTableSQL = 
        "CREATE TABLE IF NOT EXISTS users ("
        "id INTEGER PRIMARY KEY AUTOINCREMENT, "
        "name TEXT NOT NULL, "
        "age INTEGER, "
        "email TEXT)";
    
    if (!dbHelper.exec(createTableSQL)) {
        std::cout << "创建表失败!" << std::endl;
    } else {
        std::cout << "创建表成功!" << std::endl;
    }
    
    // 4. 插入测试数据
    std::cout << "\n3. 插入测试数据..." << std::endl;
    
    // 先清空表,避免重复数据
    dbHelper.exec("DELETE FROM users");
    
    // 插入多条记录
    std::string insertSQL1 = "INSERT INTO users (name, age, email) VALUES ('张三', 25, 'zhangsan@example.com')";
    std::string insertSQL2 = "INSERT INTO users (name, age, email) VALUES ('李四', 30, 'lisi@example.com')";
    std::string insertSQL3 = "INSERT INTO users (name, age, email) VALUES ('王五', 35, NULL)";
    
    if (dbHelper.exec(insertSQL1) && 
        dbHelper.exec(insertSQL2) && 
        dbHelper.exec(insertSQL3)) {
        std::cout << "插入测试数据成功!" << std::endl;
    } else {
        std::cout << "插入测试数据失败!" << std::endl;
    }
    
    // 5. 查询数据
    std::cout << "\n4. 查询所有用户数据..." << std::endl;
    std::string queryPrefix = "查询结果: ";
    std::string selectSQL = "SELECT * FROM users";
    
    if (!dbHelper.exec(selectSQL, callback, &queryPrefix)) {
        std::cout << "查询数据失败!" << std::endl;
    }
    
    // 6. 条件查询
    std::cout << "\n5. 查询年龄大于28的用户..." << std::endl;
    std::string conditionPrefix = "条件查询: ";
    std::string conditionSQL = "SELECT name, age FROM users WHERE age > 28";
    
    if (!dbHelper.exec(conditionSQL, callback, &conditionPrefix)) {
        std::cout << "条件查询失败!" << std::endl;
    }
    
    // 7. 更新数据
    std::cout << "\n6. 更新数据..." << std::endl;
    std::string updateSQL = "UPDATE users SET age = 26 WHERE name = '张三'";
    
    if (dbHelper.exec(updateSQL)) {
        std::cout << "更新数据成功!" << std::endl;
    } else {
        std::cout << "更新数据失败!" << std::endl;
    }
    
    // 8. 验证更新结果
    std::cout << "\n7. 验证更新结果..." << std::endl;
    std::string verifyPrefix = "更新后: ";
    std::string verifySQL = "SELECT * FROM users WHERE name = '张三'";
    
    if (!dbHelper.exec(verifySQL, callback, &verifyPrefix)) {
        std::cout << "验证更新结果失败!" << std::endl;
    }
    
    // 9. 删除数据
    std::cout << "\n8. 删除email为NULL的记录..." << std::endl;
    std::string deleteSQL = "DELETE FROM users WHERE email IS NULL";
    
    if (dbHelper.exec(deleteSQL)) {
        std::cout << "删除数据成功!" << std::endl;
    } else {
        std::cout << "删除数据失败!" << std::endl;
    }
    
    // 10. 查询最终结果
    std::cout << "\n9. 最终数据..." << std::endl;
    std::string finalPrefix = "最终结果: ";
    std::string finalSQL = "SELECT * FROM users";
    
    if (!dbHelper.exec(finalSQL, callback, &finalPrefix)) {
        std::cout << "查询最终数据失败!" << std::endl;
    }
    
    std::cout << "\n=== 数据库测试完成 ===" << std::endl;
    std::cout << "数据库文件 'test.db' 已创建,可以使用 SQLite 工具查看。" << std::endl;
    
    // 注意:数据库连接会在dbHelper析构时自动关闭
    return 0;
}
相关推荐
0和1的舞者1 小时前
《从静态页到自定义 Header:Spring MVC 响应能力通关指南》
java·学习·web·header·spirng
Zzzzzxl_1 小时前
互联网大厂Java/Agent面试实战:微服务、RAG与Agent化实战(含答疑解析)
java·jvm·spring boot·agent·milvus·rag·microservices
卿雪1 小时前
MySQL【索引】:索引的概念与分类
java·数据库·python·mysql·adb·golang
kong@react1 小时前
springbpoot项目,富文本,xss脚本攻击防护,jsoup
java·前端·spring boot·xss
Zzzzzxl_1 小时前
互联网大厂Java/Agent面试实战:Spring Boot、JVM、微服务、Kafka与AI Agent场景问答
java·jvm·spring boot·redis·ai·kafka·microservices
菜择贰1 小时前
为IDEA创建Linux桌面快捷方式
java·linux·intellij-idea
未若君雅裁1 小时前
JVM实战总结笔记
java·jvm·笔记
廋到被风吹走3 小时前
【Spring】Spring Data JPA Repository 自动实现机制深度解析
java·后端·spring
MX_93593 小时前
Spring中Bean的配置(一)
java·后端·spring