数据库访问的重构:从直接调用到抽象层、连接池实现、ORM模式在C语言中的尝试
说实话,数据库访问这块,是我在遗留系统里见过最"野"的代码。
很多老项目里,SQL语句直接散落在业务逻辑中,mysql_query()、sqlite3_exec()满天飞。改一个表结构,得翻遍几十个文件。我接手过一个计费系统,里面光SELECT * FROM users WHERE id = %d这种写法就出现了四十多次。每次加字段,都得祈祷别漏改哪个地方。
这一章,我们就来聊聊怎么把数据库访问从"野蛮生长"变成"井井有条"。
第一步:从直接调用到抽象层
先看一个典型的"反面教材":
// 业务代码里直接写SQL
int get_user_balance(int user_id) {
MYSQL *conn = mysql_init(NULL);
mysql_real_connect(conn, "localhost", "root", "pass", "db", 0, NULL, 0);
char sql[256];
sprintf(sql, "SELECT balance FROM users WHERE id = %d", user_id);
mysql_query(conn, sql);
MYSQL_RES *res = mysql_store_result(conn);
MYSQL_ROW row = mysql_fetch_row(res);
int balance = row ? atoi(row[0]) : 0;
mysql_free_result(res);
mysql_close(conn);
return balance;
}
这段代码的问题在哪?
- 连接管理、SQL拼接、结果解析全混在一起
- 没法单元测试——你总不能真的连数据库测吧?
- 换数据库?得把所有
mysql_*函数全改一遍
我建议的做法是:先抽象出一个数据访问层(DAL)。
// db_interface.h — 抽象接口
typedef struct db_connection db_connection_t;
typedef struct {
db_connection_t* (*connect)(const char *host, int port,
const char *user, const char *pass);
void (*close)(db_connection_t *conn);
int (*query)(db_connection_t *conn, const char *sql,
result_set_t *result);
} db_driver_t;
// 业务代码只依赖这个接口
int get_user_balance(db_driver_t *driver, int user_id) {
char sql[256];
snprintf(sql, sizeof(sql),
"SELECT balance FROM users WHERE id = %d", user_id);
result_set_t result;
if (driver->query(driver->connect(...), sql, &result) != 0) {
return -1; // 错误处理
}
return result_get_int(&result, 0, 0);
}
你看,业务代码不再关心底层是MySQL还是PostgreSQL。换数据库?写个新的db_driver_t实现就行。我在一个物联网项目中就用这套方案,从SQLite平滑迁移到了MySQL,只改了一行初始化代码。
核心原则:业务逻辑不应该知道它用的是哪种数据库。它只应该知道"有个地方能存数据、能查数据"。
第二步:连接池——别再每次请求都建连接了
很多C语言新手会犯一个错误:每次数据库操作都新建一个连接,用完就关。
为什么不好?
- TCP三次握手 + MySQL认证,一次连接耗时几十毫秒
- 高并发下,数据库服务器会被连接风暴打垮
- 连接数过多,数据库内存暴涨
我见过一个极端案例:某支付系统压测时,每秒创建300个连接,MySQL直接OOM挂了。
解决方案就是连接池。说白了,就是预先创建一批连接,用的时候借,用完还。
// 连接池核心结构
typedef struct {
db_connection_t **connections; // 连接数组
int *in_use; // 使用标记
int pool_size; // 池大小
pthread_mutex_t mutex;
pthread_cond_t cond;
} connection_pool_t;
// 从池中借一个连接
db_connection_t* pool_get_connection(connection_pool_t *pool) {
pthread_mutex_lock(&pool->mutex);
while (1) {
for (int i = 0; i < pool->pool_size; i++) {
if (!pool->in_use[i]) {
pool->in_use[i] = 1;
pthread_mutex_unlock(&pool->mutex);
return pool->connections[i];
}
}
// 所有连接都在用,等待
pthread_cond_wait(&pool->cond, &pool->mutex);
}
}
// 归还连接
void pool_return_connection(connection_pool_t *pool,
db_connection_t *conn) {
pthread_mutex_lock(&pool->mutex);
for (int i = 0; i < pool->pool_size; i++) {
if (pool->connections[i] == conn) {
pool->in_use[i] = 0;
pthread_cond_signal(&pool->cond);
break;
}
}
pthread_mutex_unlock(&pool->mutex);
}
经验之谈:连接池大小不是越大越好。我一般按"CPU核心数 × 2 + 磁盘数"来估算初始值。然后通过压测微调。曾经有个项目,池子设了200个连接,结果数据库CPU被打满,降到32个反而吞吐量更高。
第三步:ORM模式在C语言中的尝试
ORM(对象关系映射)在Java、Python里很常见。C语言能不能搞?能,但得换个思路。
C没有反射,没有运行时类型信息。所以不能像Hibernate那样自动映射。但我们可以做"半自动"的ORM——用宏和代码生成来减少重复劳动。
// 定义实体
typedef struct {
int id;
char name[64];
double balance;
time_t created_at;
} User;
// ORM宏:生成CRUD函数
#define DEFINE_ORM_CRUD(TYPE, TABLE, FIELDS) \
int TYPE##_save(TYPE *entity) { \
char sql[1024]; \
snprintf(sql, sizeof(sql), \
"INSERT INTO " TABLE " VALUES " FIELDS, \
entity->id, entity->name, entity->balance); \
return db_execute(sql); \
} \
\
int TYPE##_find_by_id(TYPE *entity, int id) { \
char sql[256]; \
snprintf(sql, sizeof(sql), \
"SELECT * FROM " TABLE " WHERE id = %d", id); \
result_set_t *rs = db_query(sql); \
if (!rs || result_set_next(rs) != 0) return -1; \
entity->id = result_get_int(rs, 0); \
strcpy(entity->name, result_get_string(rs, 1)); \
entity->balance = result_get_double(rs, 2); \
return 0; \
}
// 使用
DEFINE_ORM_CRUD(User, "users", "%d,'%s',%f")
User u;
u.id = 1001;
strcpy(u.name, "张三");
u.balance = 9999.99;
User_save(&u); // 自动生成INSERT语句
User found;
User_find_by_id(&found, 1001); // 自动生成SELECT
嗯,这里要注意:宏展开的SQL有注入风险。我一般会在宏里加一层参数化检查,或者强制使用预处理语句。
避坑指南:我曾经在一个项目里过度依赖ORM宏,结果宏展开后的代码有几千行,编译慢得要命,调试时根本看不清实际执行的SQL。后来我加了一个"调试模式"——定义DEBUG_SQL宏后,所有生成的SQL都会打印到日志里。这个习惯我一直保留到现在。
整体架构图
下面这张图展示了从业务代码到数据库的完整调用链路:
从这张图可以看得很清楚:业务代码只跟ORM层打交道,ORM层调用DAL接口,DAL从连接池拿连接,最后才落到具体的数据库。每一层各司其职,改一层不影响其他层。
总结一下
数据库访问重构,说白了就是三件事:
- 抽象接口:让业务代码不依赖具体数据库
- 连接池:复用连接,别让数据库被连接淹死
- 半自动ORM:用宏或代码生成减少重复的CRUD代码
我在好几个项目里实践过这套方案。最明显的变化是:以前改个表结构要改十几个文件,现在只需要改实体定义和ORM宏。测试也好写了——mock掉DAL接口,业务逻辑随便测。
当然,C语言的ORM做不到Java Hibernate那么"智能"。但话说回来,C语言的项目通常对性能要求更高,轻量级的半自动ORM反而更合适。你想想看,一个嵌入式系统里跑Hibernate?那画面太美我不敢看。
一句话记住:数据库访问重构的目标不是"用上高大上的技术",而是"让改数据库的人少掉几根头发"。
公众号:蓝海资料掘金营,微信deep3321