27、MySQL存储过程调用:CALL语句、参数传递、结果集处理、错误处理
存储过程这东西,说白了就是数据库里的一段预编译脚本。你把它写好,存起来,以后想用就CALL一下。我刚开始做嵌入式数据库开发时,总觉得存储过程是DBA的事,后来在项目中频繁处理大量业务逻辑,才发现这东西是真香——减少网络往返、封装复杂逻辑、还能统一管理权限。
今天咱们就聊聊怎么在C语言里调用MySQL存储过程。嗯,这里要注意,不是所有场景都适合用存储过程,但一旦用上了,你得知道怎么传参数、怎么拿结果集、怎么处理错误。
一、CALL语句的基本用法
在MySQL客户端里,调用存储过程很简单:
CALL proc_name(param1, param2, ...);
但在C语言里,你得通过MySQL C API来执行这条语句。核心函数就两个:mysql_real_query() 和 mysql_store_result()。
举个例子,假设我们有一个存储过程叫 get_device_status,它接受一个设备ID,返回该设备的状态信息:
MYSQL *conn = mysql_init(NULL);
mysql_real_connect(conn, "localhost", "user", "pass", "iot_db", 0, NULL, 0);
// 调用存储过程
const char *query = "CALL get_device_status(1001)";
if (mysql_real_query(conn, query, strlen(query)) != 0) {
fprintf(stderr, "查询失败: %s\n", mysql_error(conn));
return -1;
}
// 获取结果集
MYSQL_RES *result = mysql_store_result(conn);
if (result) {
MYSQL_ROW row;
while ((row = mysql_fetch_row(result))) {
printf("设备ID: %s, 状态: %s\n", row[0], row[1]);
}
mysql_free_result(result);
}
mysql_close(conn);
你看,其实和普通查询没什么两样。但有个坑——存储过程可能返回多个结果集。比如过程里既有SELECT,又有UPDATE,或者调用了其他过程。这时候你只处理第一个结果集是不够的。
注意:如果存储过程返回多个结果集,你必须循环调用 mysql_next_result() 来处理所有结果,否则连接会处于不一致状态,后续查询会失败。
二、参数传递:IN、OUT、INOUT
MySQL存储过程的参数有三种类型:IN(输入)、OUT(输出)、INOUT(输入输出)。在C语言里调用时,处理方式略有不同。
2.1 IN参数
IN参数最简单,直接拼到CALL语句里就行。但要注意SQL注入问题——千万别用字符串拼接用户输入。我建议用 mysql_real_escape_string() 转义,或者用预处理语句绑定参数。
char query[256];
int device_id = 1001;
snprintf(query, sizeof(query), "CALL get_device_status(%d)", device_id);
// 或者用预处理方式
MYSQL_STMT *stmt = mysql_stmt_init(conn);
const char *call = "CALL get_device_status(?)";
mysql_stmt_prepare(stmt, call, strlen(call));
MYSQL_BIND param;
memset(¶m, 0, sizeof(param));
param.buffer_type = MYSQL_TYPE_LONG;
param.buffer = &device_id;
mysql_stmt_bind_param(stmt, ¶m);
mysql_stmt_execute(stmt);
我个人习惯用预处理方式,虽然代码多几行,但安全、可读性也好。
2.2 OUT参数
OUT参数有点特殊。你不能直接在CALL语句里拿到输出值,得用用户变量来接收:
// 先调用存储过程,把结果存入用户变量
mysql_real_query(conn, "CALL get_device_count(@cnt)", strlen("CALL get_device_count(@cnt)"));
// 再查询用户变量
mysql_real_query(conn, "SELECT @cnt", strlen("SELECT @cnt"));
MYSQL_RES *res = mysql_store_result(conn);
MYSQL_ROW row = mysql_fetch_row(res);
printf("设备总数: %s\n", row[0]);
mysql_free_result(res);
我在项目中遇到过一个问题:如果存储过程里同时有OUT参数和结果集,你得先处理结果集,再查用户变量。顺序搞反了,变量值可能还没更新。
2.3 INOUT参数
INOUT参数结合了输入和输出。用法和OUT类似,但调用前要给用户变量赋初值:
mysql_real_query(conn, "SET @val = 100", strlen("SET @val = 100"));
mysql_real_query(conn, "CALL proc_inout_example(@val)", strlen("CALL proc_inout_example(@val)"));
mysql_real_query(conn, "SELECT @val", strlen("SELECT @val"));
三、结果集处理:多结果集场景
前面提到过,存储过程可能返回多个结果集。比如下面这个场景:
CREATE PROCEDURE get_device_report(IN dev_id INT)
BEGIN
-- 第一个结果集:设备基本信息
SELECT id, name, type FROM devices WHERE id = dev_id;
-- 第二个结果集:最近10条日志
SELECT log_time, message FROM device_logs
WHERE device_id = dev_id ORDER BY log_time DESC LIMIT 10;
-- 第三个结果集:统计信息
SELECT COUNT(*) AS total_logs, MAX(log_time) AS last_log
FROM device_logs WHERE device_id = dev_id;
END
在C语言里,你得这样处理:
if (mysql_real_query(conn, "CALL get_device_report(1001)", ...) != 0) {
// 错误处理
return;
}
int status = 0;
do {
MYSQL_RES *result = mysql_store_result(conn);
if (result) {
// 处理当前结果集
int num_fields = mysql_num_fields(result);
MYSQL_ROW row;
while ((row = mysql_fetch_row(result))) {
for (int i = 0; i < num_fields; i++) {
printf("%s\t", row[i] ? row[i] : "NULL");
}
printf("\n");
}
mysql_free_result(result);
} else {
// 如果没有结果集,检查是否有错误
if (mysql_errno(conn) != 0) {
fprintf(stderr, "结果集错误: %s\n", mysql_error(conn));
}
}
// 跳到下一个结果集
status = mysql_next_result(conn);
} while (status == 0);
if (status > 0) {
fprintf(stderr, "处理结果集时出错: %s\n", mysql_error(conn));
}
小技巧: mysql_next_result() 返回0表示还有下一个结果集,返回-1表示没有更多结果集,返回大于0表示出错。这个返回值判断很关键,我见过不少新手直接用 while(mysql_next_result(conn) == 0) 导致死循环的。
四、错误处理:别让异常静默
调用存储过程时,错误可能来自多个层面:
- 连接错误:数据库连不上,或者连接超时
- 语法错误:CALL语句写错了,或者存储过程不存在
- 运行时错误:存储过程内部出异常,比如除零、违反约束
- 结果集错误:处理结果集时发生问题
我建议你封装一个统一的错误处理函数:
void handle_mysql_error(MYSQL *conn, const char *context) {
fprintf(stderr, "[%s] 错误代码: %d, 错误信息: %s\n",
context, mysql_errno(conn), mysql_error(conn));
// 根据错误码做不同处理
switch (mysql_errno(conn)) {
case CR_SERVER_GONE_ERROR:
case CR_SERVER_LOST:
// 连接丢失,需要重连
printf("连接丢失,尝试重连...\n");
// 重连逻辑
break;
case 1054: // Unknown column
case 1146: // Table doesn't exist
// 结构错误,需要告警
printf("数据库结构异常,请检查表结构\n");
break;
default:
// 其他错误,记录日志
break;
}
}
我曾经在一个物联网项目中,因为没处理 CR_SERVER_GONE_ERROR,导致设备上报数据时连接已经断开,程序还在傻等结果集,最后内存泄漏、线程卡死。嗯,从那以后我再也不敢忽略错误处理了。
五、知识体系总览
下面这张图总结了存储过程调用的核心流程:
六、完整示例:带错误处理的存储过程调用
最后,给你一个完整的函数模板。我在多个项目中都用这个结构,稳定可靠:
int call_stored_proc(MYSQL *conn, int device_id, char *status_out, size_t status_len) {
// 1. 参数校验
if (!conn || device_id <= 0 || !status_out) {
return -1;
}
// 2. 准备调用
char query[256];
snprintf(query, sizeof(query),
"CALL get_device_status_ext(%d, @status)", device_id);
// 3. 执行
if (mysql_real_query(conn, query, strlen(query)) != 0) {
handle_mysql_error(conn, "CALL get_device_status_ext");
return -1;
}
// 4. 处理结果集(如果有)
int has_more = 0;
do {
MYSQL_RES *result = mysql_store_result(conn);
if (result) {
// 处理结果集...
mysql_free_result(result);
}
has_more = (mysql_next_result(conn) == 0);
} while (has_more);
// 5. 读取OUT参数
if (mysql_real_query(conn, "SELECT @status", 14) != 0) {
handle_mysql_error(conn, "SELECT @status");
return -1;
}
MYSQL_RES *res = mysql_store_result(conn);
if (res) {
MYSQL_ROW row = mysql_fetch_row(res);
if (row && row[0]) {
strncpy(status_out, row[0], status_len - 1);
status_out[status_len - 1] = '\0';
}
mysql_free_result(res);
}
return 0;
}
核心总结:
- CALL语句在C语言里就是普通查询,但要注意多结果集处理
- OUT参数通过用户变量间接获取,顺序很重要
- 错误处理不能偷懒,连接丢失、结果集异常都要覆盖
- 预处理语句绑定参数,既安全又高效
好了,存储过程调用这块就聊到这儿。你想想看,其实核心就那几步:连接、准备、执行、处理结果、检查错误。把每一步的边界情况想清楚,代码自然就稳了。
公众号:蓝海资料掘金营,微信deep3321