99、数据库编程最佳实践:编码规范、性能模式、错误处理模式、设计模式
做了这么多年嵌入式数据库开发,我踩过的坑比写过的代码还多。今天聊的这几个话题——编码规范、性能模式、错误处理、设计模式——说白了就是让你少走弯路。每个都是我用实际项目换来的教训,你仔细看看,肯定用得上。
一、编码规范:看着舒服,跑着稳当
我见过最头疼的代码,就是那种变量名随便起、缩进乱七八糟的。你想想看,一个团队里如果有人乱写,后期维护简直是噩梦。
命名规则
- 表名:小写+下划线,比如
user_info、order_detail - 字段名:同样小写+下划线,比如
create_time、is_active - C语言变量:我习惯用
tbl_前缀表示表结构,p_表示指针参数
核心原则:命名要自解释。看到名字就知道它存什么、干什么用。
代码布局
/* 好的例子 */
static int db_insert_user(sqlite3 *db, const char *name, int age)
{
int rc;
sqlite3_stmt *stmt = NULL;
const char *sql = "INSERT INTO user(name, age) VALUES(?, ?)";
rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
if (rc != SQLITE_OK) {
return rc;
}
sqlite3_bind_text(stmt, 1, name, -1, SQLITE_STATIC);
sqlite3_bind_int(stmt, 2, age);
rc = sqlite3_step(stmt);
sqlite3_finalize(stmt);
return rc;
}
我个人习惯在函数之间空两行,逻辑块之间空一行。别小看这些细节,读代码的时候舒服多了。
二、性能模式:别让数据库拖后腿
嵌入式设备资源有限,数据库性能优化是必修课。我在一个物联网项目里遇到过,数据量才几万条,查询却慢得像蜗牛。后来一查,索引没建对。
索引使用策略
| 场景 | 建议 | 原因 |
|---|---|---|
| 频繁查询的字段 | 建索引 | 加速检索 |
| 经常排序的字段 | 建索引 | 避免文件排序 |
| 写多读少的表 | 少建索引 | 索引维护有开销 |
| 小表(<1000行) | 不建索引 | 全表扫描更快 |
小技巧:用 EXPLAIN 看查询计划。我每次写复杂查询都会跑一遍,看看有没有走全表扫描。
批量操作
逐条插入数据?千万别。用事务包起来,性能能提升几十倍。
/* 慢的方式 */
for (int i = 0; i < 10000; i++) {
sqlite3_exec(db, "INSERT INTO t VALUES(1)", NULL, NULL, NULL);
}
/* 快的方式 */
sqlite3_exec(db, "BEGIN", NULL, NULL, NULL);
for (int i = 0; i < 10000; i++) {
sqlite3_exec(db, "INSERT INTO t VALUES(1)", NULL, NULL, NULL);
}
sqlite3_exec(db, "COMMIT", NULL, NULL, NULL);
嗯,这里要注意:事务别开太大,否则回滚会很慢。我一般控制在1000条左右一个事务。
三、错误处理模式:别让程序静默死掉
我曾经犯过一个低级错误——没检查返回值。结果程序跑着跑着突然挂了,查了半天才发现是数据库磁盘满了。
错误处理三原则
- 检查每个API的返回值:SQLite的每个函数都有返回码,别偷懒
- 区分致命错误和可恢复错误:磁盘满可以重试,语法错误直接报
- 记录错误上下文:光说"数据库错误"没用,要带上SQL语句和错误码
警告:千万不要在错误处理里直接 exit(1)。嵌入式设备可能需要优雅降级,比如切换到只读模式。
/* 正确的错误处理模式 */
int safe_query(sqlite3 *db, const char *sql)
{
int rc = sqlite3_exec(db, sql, NULL, NULL, NULL);
if (rc != SQLITE_OK) {
/* 记录详细错误 */
fprintf(stderr, "[DB_ERROR] sql=%s, rc=%d, msg=%s\n",
sql, rc, sqlite3_errmsg(db));
/* 根据错误码决定处理方式 */
if (rc == SQLITE_FULL) {
/* 磁盘满,尝试清理旧数据 */
return clean_old_data(db);
} else if (rc == SQLITE_BUSY) {
/* 锁冲突,重试 */
return retry_query(db, sql, 3);
}
return rc;
}
return SQLITE_OK;
}
四、设计模式:架构稳了,代码就稳了
数据库编程里,我用的最多的模式就三个。每个都是实战中提炼出来的。
1. 单例模式
嵌入式设备通常只有一个数据库连接。用单例模式管理,避免重复打开关闭。
static sqlite3 *g_db = NULL;
sqlite3* get_db_instance(void)
{
if (g_db == NULL) {
sqlite3_open("/data/app.db", &g_db);
}
return g_db;
}
2. 仓储模式
把数据库操作封装成独立的模块。业务逻辑不直接写SQL,而是调用仓储函数。
/* 仓储层 */
int user_repo_insert(const user_t *user)
{
/* 内部处理SQL和参数绑定 */
}
int user_repo_find_by_id(int id, user_t *out)
{
/* 查询逻辑封装在这里 */
}
这样做的好处是:哪天换数据库了,只改仓储层就行。我在一个项目里从SQLite换到Berkeley DB,就改了三个文件。
3. 观察者模式
数据变更时通知其他模块。比如配置表改了,自动刷新缓存。
typedef void (*db_change_cb)(const char *table_name);
static db_change_cb callbacks[10];
static int cb_count = 0;
void db_register_callback(db_change_cb cb)
{
callbacks[cb_count++] = cb;
}
void db_notify_change(const char *table)
{
for (int i = 0; i < cb_count; i++) {
callbacks[i](table);
}
}
知识体系总览
下面这张图把今天讲的内容串起来了。你对照着看,心里就有谱了。
说实话,这些最佳实践不是一天练成的。我刚开始写数据库代码时也踩过不少坑。但只要你坚持用这些模式,代码质量会肉眼可见地提升。记住:好的代码不是写出来的,是改出来的。
最后提醒一句:别想着一次把所有模式都用上。先挑一个你最薄弱的环节,比如错误处理,把它做到位。其他的慢慢来,稳扎稳打。