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_resultmysql_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条件,结果全表数据被清空。嗯,那个人就是我,刚入行的时候干的。从那以后,我养成了一个习惯:

安全删除三步法:
  1. 先用SELECT查一下WHERE条件匹配了多少行
  2. 确认无误后,把SELECT改成DELETE
  3. 执行前再检查一遍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 核心流程总结

最后,我用一张图把今天讲的核心流程串起来。你写代码时照着这个流程走,基本不会出错。

预处理语句核心流程 1. mysql_stmt_init 2. mysql_stmt_prepare 3. mysql_stmt_bind_param 4. mysql_stmt_execute 是SELECT吗? mysql_stmt_bind_result mysql_stmt_fetch mysql_stmt_affected_rows mysql_stmt_close 循环结束后

嗯,流程其实不复杂。初始化、准备、绑定参数、执行,然后根据操作类型决定是取结果还是看影响行数。最后别忘了关闭语句句柄。我见过不少内存泄漏,就是因为忘了调 mysql_stmt_close

好了,今天的内容就到这里。记住一句话:能用预处理语句就别拼SQL。这是我在无数个加班夜里总结出来的教训。

公众号:蓝海资料掘金营,微信deep3321