第67例:SQLite3——集成SQLite3数据库

说实话,嵌入式数据库这块,SQLite3 几乎是绕不开的选择。我最早接触它是在一个工控项目里,设备上跑不了 MySQL,又不想用文本文件存数据——你想想看,几十万条日志用文本存,查起来得多痛苦?后来换成 SQLite3,整个世界清净了。

这一讲,我们就聊聊怎么在 CMake 项目里优雅地集成 SQLite3。说白了,就是教会 CMake 找到 SQLite3 的头文件和库,然后链接到你的目标上。

1. 两种集成方式

集成 SQLite3 有两种主流做法:

  • 系统安装版:通过包管理器安装 libsqlite3-dev,然后 CMake 用 find_package 找到它。
  • 源码编译版:把 sqlite3.c 和 sqlite3.h 直接放到项目里,自己编译。

我个人更推荐第二种。为什么?因为跨平台时你不需要依赖系统环境,而且 SQLite3 的源码就一个 .c 文件,编译起来飞快。我在一个嵌入式 Linux 项目里就吃过系统版本不一致的亏——开发机上 SQLite3 是 3.36,目标板上是 3.28,结果有个 JSON 函数用不了。从那以后,我但凡涉及 SQLite3 的项目,一律源码集成。

2. 系统安装版:find_package 方式

如果你的环境允许安装系统库,那用 find_package 最省事。先安装:

# Ubuntu/Debian
sudo apt install libsqlite3-dev

# macOS
brew install sqlite3

# CentOS/RHEL
sudo yum install sqlite-devel

然后在 CMakeLists.txt 里这样写:

cmake_minimum_required(VERSION 3.16)
project(SQLiteDemo)

find_package(SQLite3 REQUIRED)

add_executable(myapp main.cpp)
target_link_libraries(myapp PRIVATE SQLite::SQLite3)

嗯,这里要注意:SQLite::SQLite3 是 CMake 3.14 之后才支持的导入目标。如果你用的 CMake 版本比较老,就得手动处理:

# 兼容老版本 CMake
find_package(SQLite3 REQUIRED)
add_executable(myapp main.cpp)
target_include_directories(myapp PRIVATE ${SQLite3_INCLUDE_DIRS})
target_link_libraries(myapp PRIVATE ${SQLite3_LIBRARIES})

我个人习惯用导入目标的方式,代码更干净,而且自动处理了传递依赖。

3. 源码编译版:FetchContent 方式

这才是真正的「一次配置,到处编译」。用 CMake 的 FetchContent 模块,直接从 GitHub 拉取 SQLite3 源码:

cmake_minimum_required(VERSION 3.16)
project(SQLiteEmbedded)

include(FetchContent)

FetchContent_Declare(
    sqlite3
    GIT_REPOSITORY https://github.com/sqlite/sqlite.git
    GIT_TAG version-3.45.0
    GIT_SHALLOW TRUE
)

# 注意:SQLite3 官方仓库没有 CMakeLists.txt
# 我们需要自己创建一个小型 CMake 项目来编译它
file(WRITE ${CMAKE_BINARY_DIR}/sqlite3_cmake/CMakeLists.txt [[
cmake_minimum_required(VERSION 3.16)
project(sqlite3 C)
add_library(sqlite3 STATIC sqlite3.c)
target_include_directories(sqlite3 PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
]])

FetchContent_GetProperties(sqlite3)
if(NOT sqlite3_POPULATED)
    FetchContent_Populate(sqlite3)
    # 把我们的 CMakeLists.txt 复制到源码目录
    configure_file(
        ${CMAKE_BINARY_DIR}/sqlite3_cmake/CMakeLists.txt
        ${sqlite3_SOURCE_DIR}/CMakeLists.txt
        COPYONLY
    )
    add_subdirectory(${sqlite3_SOURCE_DIR} ${sqlite3_BINARY_DIR})
endif()

add_executable(myapp main.cpp)
target_link_libraries(myapp PRIVATE sqlite3)

等等,你可能会问:「为什么官方仓库没有 CMakeLists.txt?」

因为 SQLite3 官方推荐的是用他们提供的 configure 脚本或者直接编译 amalgamation 文件。不过我们作为 CMake 用户,自己写一个也不是什么难事。我在一个 CI/CD 环境里就这么干过,效果很好。

4. 更简单的源码集成:直接拷贝

如果你不想折腾 FetchContent,最简单粗暴的方式就是直接把 sqlite3.c 和 sqlite3.h 放到项目里:

project/
├── CMakeLists.txt
├── third_party/
│   ├── sqlite3.c
│   └── sqlite3.h
└── src/
    └── main.cpp

CMakeLists.txt 这样写:

cmake_minimum_required(VERSION 3.16)
project(SQLiteLocal)

add_library(sqlite3 STATIC third_party/sqlite3.c)
target_include_directories(sqlite3 PUBLIC third_party)

add_executable(myapp src/main.cpp)
target_link_libraries(myapp PRIVATE sqlite3)

这种方式最可控,也最容易理解。我刚开始用 SQLite3 时就是这么干的,后来才慢慢过渡到 FetchContent。

5. 核心知识体系

下面这张图帮你理清思路:

SQLite3 集成方案 系统安装版 源码编译版 find_package(SQLite3 REQUIRED) target_link_libraries(... SQLite::SQLite3) FetchContent 自动拉取 add_library(sqlite3 STATIC sqlite3.c) 推荐:源码编译版(跨平台兼容性最好)

6. 实战:一个完整的例子

我们来写一个完整的 demo,演示怎么用 SQLite3 创建表、插入数据、查询数据。假设你用的是源码集成方式:

// main.cpp
#include <iostream>
#include <sqlite3.h>

int main() {
    sqlite3* db;
    char* errMsg = nullptr;
    
    // 打开数据库(不存在则创建)
    int rc = sqlite3_open("test.db", &db);
    if (rc != SQLITE_OK) {
        std::cerr << "打开数据库失败: " << sqlite3_errmsg(db) << std::endl;
        return 1;
    }
    
    // 创建表
    const char* createSQL = 
        "CREATE TABLE IF NOT EXISTS users ("
        "  id INTEGER PRIMARY KEY AUTOINCREMENT,"
        "  name TEXT NOT NULL,"
        "  age INTEGER"
        ");";
    
    rc = sqlite3_exec(db, createSQL, nullptr, nullptr, &errMsg);
    if (rc != SQLITE_OK) {
        std::cerr << "创建表失败: " << errMsg << std::endl;
        sqlite3_free(errMsg);
        sqlite3_close(db);
        return 1;
    }
    
    // 插入数据
    const char* insertSQL = 
        "INSERT INTO users (name, age) VALUES ('张三', 28);"
        "INSERT INTO users (name, age) VALUES ('李四', 32);";
    
    rc = sqlite3_exec(db, insertSQL, nullptr, nullptr, &errMsg);
    if (rc != SQLITE_OK) {
        std::cerr << "插入数据失败: " << errMsg << std::endl;
        sqlite3_free(errMsg);
    }
    
    // 查询数据
    const char* querySQL = "SELECT id, name, age FROM users;";
    rc = sqlite3_exec(db, querySQL, 
        [](void* data, int cols, char** values, char** colNames) -> int {
            for (int i = 0; i < cols; ++i) {
                std::cout << colNames[i] << ": " << (values[i] ? values[i] : "NULL") << " ";
            }
            std::cout << std::endl;
            return 0;
        }, 
        nullptr, &errMsg);
    
    if (rc != SQLITE_OK) {
        std::cerr << "查询失败: " << errMsg << std::endl;
        sqlite3_free(errMsg);
    }
    
    sqlite3_close(db);
    return 0;
}

这段代码里,我用了一个 lambda 作为回调函数来处理查询结果。这是 C++11 之后才有的写法,我个人觉得比传统的静态函数回调要清晰得多。

7. 编译和运行

假设你的项目结构是这样的:

project/
├── CMakeLists.txt
├── third_party/
│   ├── sqlite3.c
│   └── sqlite3.h
└── src/
    └── main.cpp

CMakeLists.txt 内容:

cmake_minimum_required(VERSION 3.16)
project(SQLiteDemo)

add_library(sqlite3 STATIC third_party/sqlite3.c)
target_include_directories(sqlite3 PUBLIC third_party)

# 注意:SQLite3 需要一些编译定义
target_compile_definitions(sqlite3 PRIVATE
    SQLITE_ENABLE_COLUMN_METADATA
    SQLITE_ENABLE_FTS5
    SQLITE_ENABLE_JSON1
)

add_executable(myapp src/main.cpp)
target_link_libraries(myapp PRIVATE sqlite3)

然后编译运行:

mkdir build && cd build
cmake ..
make
./myapp

输出应该类似:

id: 1 name: 张三 age: 28 
id: 2 name: 李四 age: 32

8. 避坑指南

我曾经踩过的坑:

  • 线程安全:SQLite3 默认是线程安全的,但需要在编译时定义 SQLITE_THREADSAFE=1。如果你在多线程环境下使用,记得检查这个宏。
  • 内存泄漏:每次调用 sqlite3_exec 后,如果 errMsg 不为 nullptr,一定要用 sqlite3_free 释放。我见过不少新手在这里漏掉。
  • 路径问题sqlite3_open 的路径是相对于程序工作目录的。在 IDE 里调试时,工作目录可能不是你想象的那样。

我的小技巧:

如果你只是临时用一下 SQLite3,可以用 sqlite3_open(":memory:") 创建内存数据库。这样数据不会持久化,适合做单元测试。我在写测试用例时经常这么干,跑完就自动销毁,省得清理临时文件。

9. 总结

集成 SQLite3 到 CMake 项目里,说白了就三步:找到源码、编译成库、链接到目标。我个人强烈推荐源码集成的方式,尤其是用 FetchContent 自动拉取——既保证了版本一致,又不需要手动维护源码文件。

如果你只是写个小工具,系统安装版也够用。但如果是商业项目或者需要跨平台分发,源码集成会让你省去很多麻烦。嗯,这就是我这些年积累下来的经验。


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