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,导致设备上报数据时连接已经断开,程序还在傻等结果集,最后内存泄漏、线程卡死。嗯,从那以后我再也不敢忽略错误处理了。

五、知识体系总览

下面这张图总结了存储过程调用的核心流程:

MySQL存储过程调用核心流程 1. 连接数据库 2. 准备CALL语句(参数绑定) 3. 执行存储过程 4. 处理结果集 / 读取OUT参数 多结果集循环 5. 错误检查与清理 关键要点 • IN参数:直接传值 • OUT参数:用用户变量 • INOUT:先SET再调用 • 多结果集:循环处理 • 错误码:CR_SERVER_GONE • 内存:及时free_result • 预处理:防SQL注入 • 连接状态:检查ping • 事务:存储过程内提交

六、完整示例:带错误处理的存储过程调用

最后,给你一个完整的函数模板。我在多个项目中都用这个结构,稳定可靠:

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