前两周在处理一个设备端采集程序的重构时,遇到一个很典型的需求:设备本地会持续生成一批记录数据,原先一直用自造的纯文本文件存着,解析逻辑越堆越乱,稍微换个字段分隔符就必须改代码,查历史记录更是只能靠肉眼扫文件。最后的处理方案很常规:引入嵌入式数据库,用C++做集成封装,把数据读写收敛成SQL操作。
“嵌入式数据库C++集成”这句话看起来简单,网上搜索也能找到一堆示例,但真正做好并不只是调一个 sqlite3_open 那么简单。库的选型、编译方式、连接生命周期、事务边界、多线程读写模型、各类字符编码问题,每一层都有坑。这篇文章我会从实际项目经验出发,把C++工程里集成嵌入式数据库的关键环节完整捋一遍,适合正在做桌面软件、设备端程序、边缘网关或工具类项目的开发者参考。
1. 嵌入式数据库怎么选:先定业务模型,再谈数据库产品
1.1 常见嵌入式数据库的定位与差异化
很多人听到嵌入式数据库,第一反应就是SQLite。SQLite确实是绝大多数场景下的默认答案,但把SQLite当成唯一选择,容易在两类项目上吃亏:一类是极大规模K-V存储场景,另一类是单机分析型大数据查询场景。
嵌入式数据库并不是特指某一种库,而是指运行在应用程序自身进程内、不需要独立部署服务、不需要网络监听和账号体系的数据库。正因为是进程内运行,它天然自带零部署、零运维、低延迟的特征。常见的选择主要有SQLite、LMDB、LevelDB、RocksDB、DuckDB这几个,各自的设计取向差别非常大。
| 数据库 | 数据模型 | 核心机制 | 典型场景 | 适用程度 |
|---|---|---|---|---|
| SQLite | 关系模型,支持SQL | B+树 + 日志 | 业务数据、配置、消息记录、本地档案 | 最通用,90%嵌入式场景可选 |
| LMDB | K-V | B+树 + mmap内存映射 | 高频读、低写、共享数据 | 读多写少、结构简单的场景 |
| LevelDB | K-V | LSM树 | 海量写入、大批量键值数据 | Google设计的存储引擎范式 |
| RocksDB | K-V,LevelDB增强版 | LSM树 + 多种压缩 | 服务端缓存、时序、日志类数据 | 偏后端重型场景 |
| DuckDB | 关系模型,列式 | 列式存储 + 向量化执行 | 数据清洗、大规模分析 | 分析型任务而非事务型任务 |
判断依据不是谁厉害,而是项目核心痛点。如果数据之间关系复杂,后续要按多个条件组合查询,希望用标准SQL减少编码量,SQLite几乎无法替代。如果只是记录几千个配置项的键值对,后续就是按key读取,SQLite也可以,但引入SQL解析和表结构约束反而属于过度设计,LMDB的mmap读取方式在这种场景下延迟低得离谱。
RocksDB定位更偏服务端嵌入式引擎,很多互联网组件用它做底层存储。在C++客户端工程里引入RocksDB意味着同时引入一套相对复杂的构建依赖和LSM配置参数,除非项目对写入吞吐有明确的高要求,否则不建议用于普通桌面应用。
1.2 用一张需求清单做决策
我在真正集成前,一般会把存储需求列成清单,逐项打勾。这份清单直接决定选型方向,避免被团队讨论带偏。
第一项是查询方式:是否需要按多个无关字段做组合查询。比如设备记录表里有时间、设备编号、错误码,要经常执行“查询某个时间段内设备A产生的所有错误码为X的记录”,这种查询用K-V型数据库实现异常痛苦,可能要把所有组合条件都拼成key前缀来维护。关系模型加SQL索引则天然支持这种业务,因此直接锁定SQLite。
第二项是写入量与并发模型:单连接有多少写入任务,是否涉及多线程并发写入。如果只有单线程顺序记录日志,SQLite轻松应对。如果有多个线程同时写,就要认真评估锁竞争,SQLite在同一时间只允许一个写者,但流程得当的情况下,QPS做到几千并不难。若单机每秒需要数十万次写入,那就需要考虑LevelDB或RocksDB这列LSM结构的库。
第三项是副本和迁移需求:是否需要把数据库文件拷贝到另一台设备接着用,文件是否允许损坏。SQLite单文件特性非常适合复制和备份,其他嵌入式数据库如LevelDB/RocksDB天生是目录结构,迁移时要把整个目录一并处理,文件损坏风险也更高。
填完清单后基本能确定方向。我做采集设备管理端时,存的是元数据和过程记录,查询经常要按设备、时间、结果状态过滤,所以SQLite是最合理的选择。如果你的项目只是程序启动时读一份配置文件、运行期写几个计数,那LMDB这类mmap库更轻快,也不用花力气设计表。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 把数据库真正集成进C++工程:源码、链接与IDE折腾
2.1 选择引入方式的三个可行路线
以SQLite为例,C++工程引入SQLite有三个主流路径。我不会说哪一个是绝对对的,得看你所在团队对构建依赖的管理习惯。
第一个路径是使用系统包管理器。Ubuntu上执行 apt install libsqlite3-dev,macOS用 brew install sqlite3,Windows上通过vcpkg安装。这种方式胜在快,但不同机器上的库版本可能不一致,如果团队交付时面向客户部署,版本漂移容易引出问题。
第二个路径是下载SQLite官方提供的amalgamation源码包,直接把 sqlite3.c 和 sqlite3.h 放进工程一起编译。这也是我目前比较倾向的方案,源码就两个核心文件,不依赖外部环境,编译出来的库与自己的程序完全匹配。官方发布页会同时提供 sqlite3.c、sqlite3.h,版本固定,后续升级替换文件即可。唯一需要留神的是不要同时把系统库和这个源码都链接进去,否则会出现符号冲突。
第三个路径是通过CMake的FetchContent在构建时自动拉取指定版本源码,类似:
cmake复制include(FetchContent)
FetchContent_Declare(
sqlite
URL https://www.sqlite.org/2024/sqlite-amalgamation-3450300.zip
SOURCE_DIR "${CMAKE_SOURCE_DIR}/third_party/sqlite"
)
FetchContent_MakeAvailable(sqlite)
add_executable(my_app main.cpp)
target_include_directories(my_app PRIVATE "${sqlite_SOURCE_DIR}")
target_link_libraries(my_app PRIVATE sqlite3)
这种方式适合需要多人协作的C++工程,依赖版本直接写在CMakeLists里,新增成员拉代码后构建即可,不需要手动装库。在CI环境里也能保证一致性,如果你有精力把第三方的包下载下来放到内网私服,那构建速度和稳定性都更有保障。
2.2 CMake集成时的链接参数与跨平台细节
引入SQLite后,CMake里经常出现三类报错:头文件找不到、函数符号链接不到、运行时缺少DLL。前两类代表编译阶段和链接阶段没有处理好,后一类则是动态库部署问题。
如果是自己编译的源码包,最简单的方法是直接把 sqlite3.c 加入目标源文件,它本质是一个无外部依赖的C源文件,跟着你的程序一起生成目标文件:
cmake复制add_executable(my_app main.cpp third_party/sqlite/sqlite3.c)
target_include_directories(my_app PRIVATE third_party/sqlite)
target_compile_definitions(my_app PRIVATE SQLITE_THREADSAFE=1)
如果选择链接预编译库,Linux下通常是 target_link_libraries(my_app PRIVATE sqlite3),Windows下如果是MSVC编译器,对应的导入库后缀是 .lib,MinGW则是 .dll.a,这里特别容易混。很多人在Linux开发生成 libsqlite3.so,Windows上用MinGW也想链接 libsqlite3.so,会直接报文件找不到,原因就是两个平台的库前缀和导入库名完全不一样。
还有一个高频问题出在32位/64位匹配。Visual Studio工程里添加了一个x86编译版本,运行程序没问题,但把配置切到x64再编译就报不明错误。反复排查才发现链接器引用了 sqlite3.lib,而它对应的是32位dll,x64程序找不到匹配符号。类似问题用源码编译可以自然规避,因为源码编译产物会和主程序架构保持一致。
不管用什么方式拿到库,集成之后建议先运行一个最小程序验证链路:
cpp复制#include <sqlite3.h>
#include <cstdio>
int main() {
sqlite3* db = nullptr;
int rc = sqlite3_open(":memory:", &db);
if (rc != SQLITE_OK) {
std::fprintf(stderr, "open error: %s\n", sqlite3_errmsg(db));
return -1;
}
std::printf("sqlite version: %s\n", sqlite3_libversion());
sqlite3_close(db);
return 0;
}
编译运行成功,才代表集成最低限度的链路是通的,后面再逐步封装其余功能。
2.3 VSCode和Visual Studio环境下的路径配置注意事项
如果你用VSCode做C++开发并配合CMake,需要在 c_cpp_properties.json 里让IntelliSense能找到核心头文件路径,否则代码里 #include <sqlite3.h> 会有红色波浪线,但编译却可能没问题。这是因为编译器实际用了 compile_commands.json 提供的路径,而IntelliSense用的是自己的配置文件。我的经验是打开CMake时开启 CMAKE_EXPORT_COMPILE_COMMANDS,让VSCode的C/C++插件读取compile_commands,比手动维护头文件路径省心得多。
Visual Studio用户一般通过“VC++目录”或NuGet引入预编译包,这里我建议把SQLite作为源码文件加入项目而非引入NuGet预编译包。原因一是预编译包装的版本不一定新,二是NuGet包往往附带版本号和路径规则,别人拷贝工程时一旦包还原失败,整个编译就卡住了。源码文件加入项目虽然看起来原始,但构建时不需要联网,可靠性反而更高。
3. 核心API实操:prepare、bind、step才是真正的日常主力
3.1 为什么不要直接在业务代码里拼SQL字符串
很多接触SQLite的C++新手,第一步搜到 sqlite3_exec,然后就把SQL语句和参数拼接进字符串,像这样:
cpp复制std::string sql = "INSERT INTO note(content, owner) VALUES('" + content + "', '" + owner + "')";
sqlite3_exec(db, sql.c_str(), nullptr, nullptr, nullptr);
这段代码一旦某个字段里包含单引号就会造成SQL语法错误,如果字段被精心构造还可能形成注入风险。单引号转义的实现很容易忘,例如内容本身就是一句话 “It's fine.”,拼进去后SQL被截断报错。另一个问题是大量字符串拼接造成的内存与时间开销不小,连续插入一万条记录时,每次都重新拼接并交给执行器解析,效率远低于预编译语句。
C接口里真正适合业务使用的是prepare系列函数:
c复制int sqlite3_prepare_v2(sqlite3* db, const char* zSql, int nByte, sqlite3_stmt** ppStmt, const char** pzTail);
int sqlite3_bind_text(sqlite3_stmt* stmt, int index, const char* value, int length, void(*destructor)(void*));
int sqlite3_step(sqlite3_stmt* stmt);
int sqlite3_reset(sqlite3_stmt* stmt);
它的工作过程很像菜谱:先配好一份可以反复使用的操作模板(prepared statement),每次执行只替换不同食材(绑定参数),然后按步骤执行。数据库引擎会缓存解析后的执行计划,不必每次重新解析SQL文本,性能和安全性都高一个量级。
3.2 用RAII封装连接与语句资源
sqlite3的标准C接口要求使用完 sqlite3_stmt 后调用 sqlite3_finalize,连接用完调用 sqlite3_close。业务代码里一旦分支变多、中途抛出异常,很容易忘记finalize和close,造成内存增长和文件占用。
C++工程集成时,我建议第一件事就是写两个轻量的RAII封装类,一个是Database,一个是Statement。不需要引入额外第三方库,思路很直接:
cpp复制class Database {
public:
explicit Database(const std::string& path) {
int rc = sqlite3_open(path.c_str(), &db_);
if (rc != SQLITE_OK) {
std::string msg = db_ ? sqlite3_errmsg(db_) : "unknown error";
if (db_) sqlite3_close(db_);
db_ = nullptr;
throw std::runtime_error("open database failed: " + msg);
}
}
~Database() {
if (db_) sqlite3_close(db_);
}
sqlite3* raw() const { return db_; }
private:
sqlite3* db_ = nullptr;
};
Statement类同理,构造时prepare,析构时finalize。绑定参数和获取结果可以先包一层,但尽量不要把所有接口都包死,保留一个返回底层 sqlite3_stmt* 的通道,方便以后直接调用更底层的C接口:
cpp复制class Statement {
public:
Statement(sqlite3* db, const char* sql) {
int rc = sqlite3_prepare_v2(db, sql, -1, &stmt_, nullptr);
if (rc != SQLITE_OK) {
std::string msg = db ? sqlite3_errmsg(db) : "unknown error";
throw std::runtime_error("prepare failed: " + msg);
}
}
~Statement() {
if (stmt_) sqlite3_finalize(stmt_);
}
void reset() {
sqlite3_reset(stmt_);
sqlite3_clear_bindings(stmt_);
}
int step() {
return sqlite3_step(stmt_);
}
void bind(int index, int value) {
sqlite3_bind_int(stmt_, index, value);
}
void bind(int index, long long value) {
sqlite3_bind_int64(stmt_, index, value);
}
void bind(int index, const std::string& value) {
sqlite3_bind_text(stmt_, index, value.data(), (int)value.size(), SQLITE_TRANSIENT);
}
sqlite3_stmt* raw() const { return stmt_; }
private:
sqlite3_stmt* stmt_ = nullptr;
};
封装里那个 SQLITE_TRANSIENT 值得多说一句。它告诉SQLite“字符串可能在执行结束前被释放,请先复制一份”。如果不传SQLITE_TRANSIENT而是传SQLITE_STATIC,SQLite就默认字符串在整个statement生命周期内一直有效,一旦bind后马上修改或释放std::string,后续step阶段拿到的数据就是不可预知的。
3.3 查询与遍历的常见误区:一次step不代表所有数据返回完成
写完Statement封装后,最容易踩的坑是拿返回码判断数据。stsqlite3_step 返回 SQLITE_ROW 表示拿到一行结果,返回 SQLITE_DONE 表示正常执行结束。执行INSERT,期望的结束码应该是SQLITE_DONE;执行SELECT,需要循环调用step直到返回SQLITE_DONE,每遇到一次SQLITE_ROW读取一行数据。
如果要把同一个预编译语句重复使用,再次执行前必须调用 sqlite3_reset,否则会在上一次的中间状态下继续执行。reset之后还要清掉旧绑定值,SQLite对重复绑定同一个位置的参数会自动覆盖,但删除型业务常出现上一轮绑定了5个参数,新一轮只绑定了3个,剩下两个仍沿用旧值的情况,所以reset后顺手调 sqlite3_clear_bindings 可以杜绝脏数据问题。
实际项目里,我常写一个查询辅助代码,把所有结果装入结构体,例如:
cpp复制struct NoteRow {
int id;
std::string content;
int64_t createdAt;
};
std::vector<NoteRow> queryNotes(sqlite3* db, const std::string& owner) {
Statement stmt(db, "SELECT id, content, created_at FROM note WHERE owner = ?");
stmt.bind(1, owner);
std::vector<NoteRow> rows;
while (sqlite3_step(stmt.raw()) == SQLITE_ROW) {
NoteRow r;
r.id = sqlite3_column_int(stmt.raw(), 0);
const unsigned char* text = sqlite3_column_text(stmt.raw(), 1);
if (text) r.content = reinterpret_cast<const char*>(text);
r.createdAt = sqlite3_column_int64(stmt.raw(), 2);
rows.push_back(std::move(r));
}
return rows;
}
注意取字符串列时,通过 sqlite3_column_text 拿到的指针指向SQLite内部缓冲区,后续调用一次step就会失效,所以必须立刻复制进std::string再往下走。在存储过程和触发器里返回大量数据时更要注意这个生命周期,不能在行对象里保留裸指针。
4. 事务、并发与WAL:数据库集成不只是C API调用
4.1 批量写入慢的根源在于隐式事务
我见过不少代码,功能完全正确,插入一千条记录却花了十几秒。问题几乎都出在没使用显式事务。SQLite每次执行一条无显式事务的INSERT,都会自动开启一个事务,写完立刻提交。提交背后是一整套文件写入和同步机制,对物理磁盘的刷盘开销极大。
解决方式是手动包一个显式事务,把大量INSERT合并到一次提交中:
cpp复制sqlite3_exec(db, "BEGIN", nullptr, nullptr, nullptr);
Statement stmt(db, "INSERT INTO note(content, owner) VALUES(?, ?)");
for (const auto& item : items) {
stmt.reset();
stmt.bind(1, item.content);
stmt.bind(2, item.owner);
int rc = stmt.step();
if (rc != SQLITE_DONE) {
sqlite3_exec(db, "ROLLBACK", nullptr, nullptr, nullptr);
throw std::runtime_error(std::string("insert failed: ") + sqlite3_errmsg(db));
}
}
sqlite3_exec(db, "COMMIT", nullptr, nullptr, nullptr);
在普通机械硬盘上,不包事务单条插入大概要几十毫秒甚至更高,包了事务后每秒可以达到数千到数万条,差别可以达到几个数量级。如果某条语句执行失败,记得执行ROLLBACK,否则事务会一直挂着,锁住整个数据库。
更稳妥的做法是把事务也封装成RAII对象,构造函数执行BEGIN,析构时如果没有显式commit就自动ROLLBACK。这样即使中间抛出异常,事务也能安全回滚,不会把数据库留在半提交状态。事务对象要显式管理,避免两个事务对象嵌套使用时把外层事务意外回滚。
4.2 多线程访问的正确姿势:连接与线程的边界
SQLite编译时默认是线程安全模式,线程安全指的是同一个 sqlite3* 连接可以被多个线程使用,但它内部靠互斥锁保护,写并发时依然会形成争用。实际项目中,如果多个线程共用同一个连接做频繁写入,你会发现性能并不乐观。
常见的设计是多线程读、单线程写,或者每个业务模块持有独立连接。C++服务端模式里,更好的方案是按线程创建连接,线程内对数据库的使用天然串行,避免锁竞争。SQLite官方文档明确过,默认模式下,一个连接对象不能同时被多个线程使用,除非开启serialized模式。工程中把代码写成“每个线程独立开库”最简单可靠,不要试图共享同一个连接。
操作系统层面还需要设置忙等待超时。当连接A拿到写锁做事务,连接B同时发起写操作时,SQLite默认会立刻返回SQLITE_BUSY。如果是预期内的高频并发访问,应该在每次建立连接后执行:
sql复制PRAGMA busy_timeout = 5000;
设置之后,当锁被占用时SQLite会自旋等待,直到5秒或获取到锁,而不是立刻把错误抛回业务层。这个值也不能设置过大,高并发场景下如果多个写者长期持锁,等待方容易排队。
4.3 WAL与synchronous参数如何取舍
默认rollback journal模式下,SQLite每写一次数据要写一份原始页,提交时锁住整个数据库,读操作会被写操作阻塞。嵌入式程序往往是采集线程持续写,UI线程偶尔读,如果读写交替频繁,用户会明显感觉查询卡顿。
切换到WAL模式大多能解决这个问题:
sql复制PRAGMA journal_mode=WAL;
PRAGMA synchronous=NORMAL;
WAL模式下写操作先追加到独立的write-ahead log文件,不直接覆盖主数据库文件。读操作可以继续读主库的历史快照,写读并行冲突大大减少。配套的 synchronous=NORMAL 在WAL模式下足够保证数据库在应用崩溃后仍然可用,只有整机断电的极端情况才可能丢失最近几次提交但不会损坏库文件。
如果你做的应用要求极高可靠性,比如金融交易日志,可以保留 synchronous=FULL;反之写吞吐要求高、数据允许最后几秒丢失,用 synchronous=OFF 会让写入速度更高。大多数设备端程序我认为NORMAL是收益最高的,既没有FULL那么慢,又比OFF安全得多。
WAL模式也有代价,目录里会出现额外的 -wal 和 -shm 文件。拷贝数据库时如果不带wal文件,可能丢失尚未合并的提交。正确做法是在拷贝前执行一次 PRAGMA wal_checkpoint(TRUNCATE) 或正常关闭连接,SQLite会在最后一个连接安全关闭时自动做checkpoint合并。设备程序如果把数据库备份到U盘,这一点极其关键,否则备份出来的库可能是残缺的。
5. C++集成中最容易踩的坑:编译、链接与运行期数据
5.1 编译链接阶段错误速查与解决思路
| 错误现象 | 可能原因 | 处理办法 |
|---|---|---|
| fatal error: sqlite3.h: No such file or directory | 头文件路径没加 | 检查target_include_directories或项目包含目录 |
undefined reference to sqlite3_open |
链接库没加或顺序不对 | 将sqlite3链接库放到依赖目标后面 |
| LNK2019: unresolved external symbol | MSVC工程没有引用lib | 配置附加依赖项或源码加入工程 |
| cannot open file sqlite3.lib | lib路径或架构不匹配 | 检查32/64位与Debug/Release配置 |
| 运行时提示缺sqlite3.dll | 动态库没有部署到运行目录 | 将dll放到exe同目录或配置PATH |
| 多定义符号sqlite3_xxx | 源码包和预编译库同时链接 | 只保留一种引入方式 |
链接顺序问题在Linux上很常见。CMake中如果写成 target_link_libraries(my_app sqlite3),编译器从左到右处理库文件,多次循环依赖时不加处理就会报未定义符号。现在CMake版本默认带 --start-group 不太可能,最稳妥的方案还是把 sqlite3.c 作为源码文件编译进目标,这样链接阶段根本不存在库顺序问题。我后来维护的多数C++工程都走源码编译路线,省心不少。
对C++工程还有一个隐蔽点:某些第三方库本身自带了一套SQLite,而且编译宏与你的工程不一致。比如下载了一个静态库,它内部用了 SQLITE_HAS_CODEC 扩展,而你的主程序没有编译这个宏,双方对内部结构体大小定义不同,调用时可能产生难以定位的内存破坏。如果项目中已经存在一个被三方模块使用的SQLite版本,强烈建议所有模块统一使用同一个引入方式,避免符号冲突。
5.2 中文路径与字符编码的坑
Windows系统中 sqlite3_open 接受的路径参数是UTF-8编码。大多数C++程序在Windows上如果直接拿窄字符的 char* 传路径,而这个路径是本地ANSI编码,一旦路径包含中文就会打不开数据库。正确做法是把程序内部的std::wstring转成UTF-8,再传给sqlite3_open:
cpp复制std::string utf8Path = U16ToUtf8(widePath);
sqlite3_open(utf8Path.c_str(), &db);
更省事的方案是用官方提供的 sqlite3_open16 接口,它接收UTF-16编码路径,在Windows上与系统原生编码天然匹配。不过这样会增加跨平台代码分支,我的做法是统一抽象一个open函数,Windows下调用 sqlite3_open16,Linux下调用 sqlite3_open。
数据库中存储文本也建议统一使用UTF-8。查询条件和写入内容都遵循UTF-8规范时,WHERE content = ? 的匹配行为才稳定。如果一部分代码写入GBK、一部分写入UTF-8,查得到数据全凭运气,且这种问题在测试阶段不一定暴露,上线后到客户环境才翻车。
5.3 时间与数据完整性经验
SQLite没有独立的时间类型,很多人会把时间存成字符串,例如“2025-05-01 12:30:00”。这个格式人类可读性不错,但排序和范围比较依赖字符串格式完全统一。如果部分代码插入“2025-5-1”这种格式,查询排序就会错乱。我更倾向于统一存Unix时间戳整数,展示层再去格式化。如果必须用字符串,就用ISO8601标准格式并保证所有写入代码经过同一个时间格式化函数,不要到处手写时间拼接。
数据库文件损坏也是一个防不胜防的问题。程序崩溃、磁盘写满、进程被强杀,都有可能导致SQLite主库损坏。集成阶段就应引入完整性检查机制,启动时可以执行 PRAGMA quick_check,损坏时有意识地采用备份恢复或重建流程。SQLite官方提供 sqlite3_stmt 的字节码,配合定期checkpoint可以把WAL文件控制在一个合理大小,避免长期运行后日志文件膨胀。
字段级结构升级也值得提前设计。软件版本更新后表结构可能要增加字段,老版本生成的数据库文件必须能平滑升级。我的做法是维护一张meta表记录schema版本,每次升级在事务里执行 ALTER TABLE 并递增版本号,绝不在用户数据库中直接删除旧列,避免丢失历史数据。
回顾我这次采集设备程序的集成过程,数据库选型其实花了大半天,代码封装只用了差不多一天。这个时间分配很说明问题:嵌入式数据库的C++集成,核心难点从来不在API记忆,而在方案选型和边界设计。如果你还在犹豫自己的模块要不要引入嵌入式数据库,我的建议是先列一遍查询场景、写入频率、文件移动方式,再决定库的类型;如果确认走SQLite路线,务必尽早把事务封装、连接生命周期和WAL参数定下来,否则后面测试越到后期,改起来代价越大。
代码层之外也有一件事值得单独提醒:不要把数据库字段直接暴露给整个业务层。设计上尽量用一个独立的repository或dao模块做访问收敛,这样将来更换数据库、调整表结构,都不需要翻遍全项目。这个习惯看着是架构洁癖,实际维护到第三年时会帮你省下大量精力。这套选择我个人实践下来效果不错,也推荐给正在做C++项目集成的同行作为参考。
