25、MySQL数据操作:INSERT、SELECT、UPDATE、DELETE的C API实现、预处理语句
说实话,MySQL的C API写起来挺有年代感的。我最早接触它是在一个嵌入式网关项目里,那时候数据库跑在ARM Linux上,资源紧张得很。你想想看,用C语言直接操作MySQL,没有ORM,没有框架,就是赤裸裸的API调用。但正是这种「裸奔」的感觉,让我对数据库底层交互有了深刻理解。
今天咱们就聊聊四个最基础的操作:增、删、改、查。嗯,也就是INSERT、SELECT、UPDATE、DELETE。我会结合预处理语句来讲,因为这才是生产环境该用的方式。
25.1 基础API:mysql_query 与 mysql_real_query
先看两个最常用的函数。我个人习惯用 mysql_real_query 而不是 mysql_query。为什么?因为前者支持二进制数据,而且能指定SQL语句长度,避免字符串截断问题。
// 基本用法对比
int mysql_query(MYSQL *mysql, const char *query);
int mysql_real_query(MYSQL *mysql, const char *query, unsigned long length);
我在项目中遇到过一个问题:用 mysql_query 执行一条很长的INSERT语句,结果字符串里包含了一个分号,导致SQL被截断。排查了半天才发现是 mysql_query 内部用 strlen 计算长度,遇到 \0 就停了。从那以后,我统一改用 mysql_real_query,自己传长度,心里踏实。
25.2 INSERT:插入数据
插入数据是最常见的操作。直接拼SQL字符串?别这么干。你想想看,如果用户输入的内容里带个单引号,你的SQL就炸了。更别提SQL注入攻击了。
正确的做法是用预处理语句。看代码:
MYSQL_STMT *stmt;
MYSQL_BIND bind[3];
char name[64];
int age;
double salary;
// 准备预处理语句
stmt = mysql_stmt_init(mysql);
const char *sql = "INSERT INTO employees (name, age, salary) VALUES (?, ?, ?)";
mysql_stmt_prepare(stmt, sql, strlen(sql));
// 绑定参数
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;
bind[2].buffer_type = MYSQL_TYPE_DOUBLE;
bind[2].buffer = &salary;
mysql_stmt_bind_param(stmt, bind);
// 设置值并执行
strcpy(name, "张三");
age = 28;
salary = 15000.0;
mysql_stmt_execute(stmt);
printf("插入成功,影响行数: %lu\n", mysql_stmt_affected_rows(stmt));
mysql_stmt_close(stmt);
mysql_stmt_execute 后,我都会检查 mysql_stmt_errno(stmt)。别偷懒,很多隐蔽错误(比如字段类型不匹配)只有通过错误码才能发现。
25.3 SELECT:查询数据
查询比插入稍微复杂一点,因为你要把结果取出来。我记得刚用C API时,总搞混 mysql_store_result 和 mysql_use_result 的区别。简单说:
- mysql_store_result:一次性把所有结果拉到客户端内存。适合数据量小的场景。
- mysql_use_result:逐行从服务器读取,不占用客户端太多内存。适合大结果集。
我个人偏好 mysql_store_result,因为操作简单,而且可以随时 mysql_data_seek 跳转到任意行。但如果你要查几十万条记录,还是用 mysql_use_result 吧,否则内存会爆。
MYSQL_STMT *stmt;
MYSQL_BIND bind_param[1], bind_result[3];
int id_param = 100;
char name[64];
int age;
double salary;
unsigned long name_len;
my_bool is_null[3];
// 准备查询
stmt = mysql_stmt_init(mysql);
const char *sql = "SELECT name, age, salary FROM employees WHERE id = ?";
mysql_stmt_prepare(stmt, sql, strlen(sql));
// 绑定输入参数
memset(bind_param, 0, sizeof(bind_param));
bind_param[0].buffer_type = MYSQL_TYPE_LONG;
bind_param[0].buffer = &id_param;
mysql_stmt_bind_param(stmt, bind_param);
// 绑定结果缓冲区
memset(bind_result, 0, sizeof(bind_result));
bind_result[0].buffer_type = MYSQL_TYPE_STRING;
bind_result[0].buffer = name;
bind_result[0].buffer_length = sizeof(name);
bind_result[0].length = &name_len;
bind_result[0].is_null = &is_null[0];
bind_result[1].buffer_type = MYSQL_TYPE_LONG;
bind_result[1].buffer = &age;
bind_result[1].is_null = &is_null[1];
bind_result[2].buffer_type = MYSQL_TYPE_DOUBLE;
bind_result[2].buffer = &salary;
bind_result[2].is_null = &is_null[2];
mysql_stmt_bind_result(stmt, bind_result);
// 执行并获取结果
mysql_stmt_execute(stmt);
mysql_stmt_store_result(stmt);
while (mysql_stmt_fetch(stmt) == 0) {
printf("name: %s, age: %d, salary: %.2f\n",
is_null[0] ? "NULL" : name,
is_null[1] ? 0 : age,
is_null[2] ? 0.0 : salary);
}
mysql_stmt_free_result(stmt);
mysql_stmt_close(stmt);
buffer_length 一定要给够。我曾经设了32字节,结果数据库里有个名字33字节,直接导致内存越界。程序没崩溃,但读出来的数据是乱码。排查了俩小时...
25.4 UPDATE:更新数据
更新操作和INSERT很像,也是用预处理语句绑定参数。但有个细节要注意:mysql_stmt_affected_rows 返回的是实际被修改的行数,不是匹配的行数。如果你UPDATE一个字段为原值,MySQL可能不会计入影响行数(取决于你用的引擎和配置)。
MYSQL_STMT *stmt;
MYSQL_BIND bind[2];
double new_salary = 18000.0;
int target_id = 100;
stmt = mysql_stmt_init(mysql);
const char *sql = "UPDATE employees SET salary = ? WHERE id = ?";
mysql_stmt_prepare(stmt, sql, strlen(sql));
memset(bind, 0, sizeof(bind));
bind[0].buffer_type = MYSQL_TYPE_DOUBLE;
bind[0].buffer = &new_salary;
bind[1].buffer_type = MYSQL_TYPE_LONG;
bind[1].buffer = &target_id;
mysql_stmt_bind_param(stmt, bind);
mysql_stmt_execute(stmt);
if (mysql_stmt_affected_rows(stmt) == 0) {
printf("注意:没有行被更新。可能ID不存在,或者salary已经是18000了。\n");
}
mysql_stmt_close(stmt);
25.5 DELETE:删除数据
删除操作要格外小心。我见过有人写DELETE语句忘了加WHERE条件,结果全表数据被清空。嗯,那个人就是我,刚入行的时候干的。从那以后,我养成了一个习惯:
- 先用SELECT查一下WHERE条件匹配了多少行
- 确认无误后,把SELECT改成DELETE
- 执行前再检查一遍SQL字符串
MYSQL_STMT *stmt;
MYSQL_BIND bind[1];
int delete_id = 200;
stmt = mysql_stmt_init(mysql);
const char *sql = "DELETE FROM employees WHERE id = ?";
mysql_stmt_prepare(stmt, sql, strlen(sql));
memset(bind, 0, sizeof(bind));
bind[0].buffer_type = MYSQL_TYPE_LONG;
bind[0].buffer = &delete_id;
mysql_stmt_bind_param(stmt, bind);
mysql_stmt_execute(stmt);
printf("删除了 %lu 行\n", mysql_stmt_affected_rows(stmt));
mysql_stmt_close(stmt);
25.6 预处理语句的优势
说了这么多,你可能觉得预处理语句比直接拼SQL麻烦多了。没错,代码量确实多了。但好处也很明显:
| 特性 | 直接拼SQL | 预处理语句 |
|---|---|---|
| SQL注入防护 | 需要手动转义 | 自动处理 |
| 二进制数据支持 | 困难 | 原生支持 |
| 重复执行性能 | 每次都要解析SQL | 只解析一次 |
| 代码可读性 | 简单直观 | 结构清晰 |
说白了,如果你写的是玩具代码,直接拼SQL没问题。但要是生产环境,尤其是涉及用户输入的场景,预处理语句是唯一正确的选择。
25.7 核心流程总结
最后,我用一张图把今天讲的核心流程串起来。你写代码时照着这个流程走,基本不会出错。
嗯,流程其实不复杂。初始化、准备、绑定参数、执行,然后根据操作类型决定是取结果还是看影响行数。最后别忘了关闭语句句柄。我见过不少内存泄漏,就是因为忘了调 mysql_stmt_close。
好了,今天的内容就到这里。记住一句话:能用预处理语句就别拼SQL。这是我在无数个加班夜里总结出来的教训。