21、MySQL预处理与事务:mysql_stmt_prepare、绑定参数、事务控制、错误码处理
说实话,很多C语言开发者写MySQL程序时,都是直接拼SQL字符串往里怼。这种做法在小项目里跑跑还行,一旦上了生产环境,问题就来了——SQL注入、性能低下、事务混乱。今天我就把预处理和事务这块掰开揉碎讲清楚。
21.1 预处理语句:为什么不用拼字符串?
先说说我个人的习惯。每次写数据库操作,我第一反应就是用预处理语句。为什么?因为安全。
你想想看,如果用户输入一个用户名叫 admin' OR '1'='1,你直接拼到SQL里会怎样?整张表的数据都暴露了。我在项目中遇到过这种惨案,还好只是测试环境,不然就等着被开除吧。
预处理语句的核心思想很简单:SQL结构固定,参数单独传递。MySQL服务器先解析SQL模板,再绑定参数执行。这样用户输入再脏,也影响不了SQL结构。
21.1.1 基本流程
预处理语句的使用分三步走:
- 准备阶段:调用
mysql_stmt_prepare()解析SQL模板 - 绑定参数:用
mysql_stmt_bind_param()把变量和占位符关联起来 - 执行:调用
mysql_stmt_execute()跑起来
来看代码:
MYSQL_STMT *stmt;
MYSQL_BIND bind[2];
char name[50];
int age;
// 1. 准备SQL模板
stmt = mysql_stmt_init(mysql);
const char *sql = "INSERT INTO users(name, age) VALUES(?, ?)";
mysql_stmt_prepare(stmt, sql, strlen(sql));
// 2. 绑定参数
memset(bind, 0, sizeof(bind));
bind[0].buffer_type = MYSQL_TYPE_STRING;
bind[0].buffer = name;
bind[0].buffer_length = sizeof(name);
bind[1].buffer_type = MYSQL_TYPE_LONG;
bind[1].buffer = &age;
mysql_stmt_bind_param(stmt, bind);
// 3. 设置值并执行
strcpy(name, "张三");
age = 25;
mysql_stmt_execute(stmt);
// 重复使用
strcpy(name, "李四");
age = 30;
mysql_stmt_execute(stmt);
mysql_stmt_close(stmt);
21.2 绑定参数的细节
绑定参数这块,坑不少。我刚开始用的时候也栽过跟头。
21.2.1 MYSQL_BIND结构体
每个参数都要填一个 MYSQL_BIND 结构体。关键字段就这几个:
| 字段 | 说明 | 常见取值 |
|---|---|---|
| buffer_type | 数据类型 | MYSQL_TYPE_LONG, MYSQL_TYPE_STRING, MYSQL_TYPE_DOUBLE |
| buffer | 指向数据的指针 | &int_var, char_array, &double_var |
| buffer_length | 缓冲区大小(字符串必填) | sizeof(buffer) |
| is_null | 是否为NULL | my_bool is_null = 0; &is_null |
| length | 实际数据长度(变长数据用) | unsigned long len; &len |
嗯,这里要注意:字符串类型必须设置 buffer_length,否则MySQL不知道从哪读到哪。我曾经忘了设这个,结果插进去的数据全是乱码,排查了半天才发现。
21.2.2 处理NULL值
数据库里经常有NULL字段。怎么传?用 is_null 字段:
my_bool is_null[2];
MYSQL_BIND bind[2];
memset(bind, 0, sizeof(bind));
// 第一个参数正常传值
bind[0].buffer_type = MYSQL_TYPE_LONG;
bind[0].buffer = &id;
bind[0].is_null = &is_null[0];
is_null[0] = 0; // 非NULL
// 第二个参数传NULL
bind[1].buffer_type = MYSQL_TYPE_STRING;
bind[1].is_null = &is_null[1];
is_null[1] = 1; // 就是NULL
21.3 事务控制:要么全做,要么全不做
事务这东西,说白了就是一组操作要么全部成功,要么全部回滚。我在做银行转账功能时深有体会——扣钱成功但加钱失败,这谁受得了?
21.3.1 基本操作
MySQL默认是自动提交模式,每条SQL都立即生效。要用手动事务,得先关掉自动提交:
// 关闭自动提交
mysql_autocommit(mysql, 0);
// 执行一组操作
mysql_query(mysql, "UPDATE accounts SET balance = balance - 100 WHERE id = 1");
mysql_query(mysql, "UPDATE accounts SET balance = balance + 100 WHERE id = 2");
// 检查结果,决定提交还是回滚
if (/* 全部成功 */) {
mysql_commit(mysql);
} else {
mysql_rollback(mysql);
}
// 恢复自动提交
mysql_autocommit(mysql, 1);
21.3.2 事务+预处理组合拳
实际项目中,事务和预处理经常一起用。比如批量转账:
MYSQL_STMT *stmt;
MYSQL_BIND bind[2];
int from_id, to_id;
double amount;
// 准备更新语句
stmt = mysql_stmt_init(mysql);
const char *sql = "UPDATE accounts SET balance = balance + ? WHERE id = ?";
mysql_stmt_prepare(stmt, sql, strlen(sql));
// 绑定参数
memset(bind, 0, sizeof(bind));
bind[0].buffer_type = MYSQL_TYPE_DOUBLE;
bind[0].buffer = &amount;
bind[1].buffer_type = MYSQL_TYPE_LONG;
bind[1].buffer = &to_id;
mysql_stmt_bind_param(stmt, bind);
// 开启事务
mysql_autocommit(mysql, 0);
// 扣钱
amount = -100.0;
to_id = 1;
mysql_stmt_execute(stmt);
// 加钱
amount = 100.0;
to_id = 2;
mysql_stmt_execute(stmt);
// 检查错误
if (mysql_errno(mysql) == 0) {
mysql_commit(mysql);
printf("转账成功\n");
} else {
mysql_rollback(mysql);
printf("转账失败: %s\n", mysql_error(mysql));
}
mysql_autocommit(mysql, 1);
mysql_stmt_close(stmt);
21.4 错误码处理:别让程序静默死掉
错误处理是数据库编程里最容易被忽视的部分。很多新手写代码,调用完 mysql_stmt_execute 就不管了,程序跑飞了都不知道。
21.4.1 常用错误码
| 错误码 | 含义 | 常见原因 |
|---|---|---|
| 1062 | 主键/唯一键重复 | 插入重复数据 |
| 1048 | 列不能为NULL | NOT NULL字段传了NULL |
| 1146 | 表不存在 | SQL写错表名 |
| 1213 | 死锁 | 并发事务互相等待 |
| 2006 | MySQL服务器已断开 | 连接超时或网络问题 |
21.4.2 错误处理模板
我个人习惯写一个统一的错误处理函数:
void check_stmt_error(MYSQL_STMT *stmt, const char *action) {
if (mysql_stmt_errno(stmt) != 0) {
fprintf(stderr, "[错误] %s 失败: %s (错误码: %d)\n",
action,
mysql_stmt_error(stmt),
mysql_stmt_errno(stmt));
// 特殊处理死锁
if (mysql_stmt_errno(stmt) == 1213) {
printf("检测到死锁,建议重试事务\n");
}
// 特殊处理连接断开
if (mysql_stmt_errno(stmt) == 2006) {
printf("连接已断开,需要重新连接\n");
}
}
}
// 使用示例
mysql_stmt_execute(stmt);
check_stmt_error(stmt, "执行转账扣款");
21.5 知识体系总览
下面这张图把本章的核心逻辑串起来了:
说白了,这三者缺一不可。预处理防注入,事务保一致,错误码处理让程序不崩。我在实际项目中,这三样东西几乎是每段数据库代码的标配。
最后提醒一句:别偷懒。每次调用MySQL API后都检查返回值,每次事务结束都检查错误码。养成这个习惯,能帮你省下无数个通宵排查的夜晚。