第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. 核心知识体系
下面这张图帮你理清思路:
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