【MySQL 进阶系列】C/C++ 如何通过客户端库访问 MySQL?从连接原理到 API 调用流程详解(附完整demo代码)

🔥 本文专栏:MySQL

🌸作者主页:努力努力再努力wz


💪 今日博客励志语录人生不是靠一次正确选择翻盘的,而是靠无数次选择之后,仍然愿意修正和前进。


思维导图

引入

我们知道,MySQL 是一个典型的多用户客户端-服务器架构的网络服务程序。MySQL 服务端会与不同客户端建立连接,而在经典的连接处理模型中,一个客户端连接通常会对应服务端的一个工作线程,用来处理该连接上发送过来的 SQL 请求。

在此之前,我们更多是通过 Linux 下的 MySQL 客户端与 MySQL 服务端进行交互。也就是说,程序员手动在客户端中输入一条条 SQL 语句,这些 SQL 语句会被客户端封装成符合 MySQL 协议的请求报文,然后通过网络发送给 MySQL 服务端。服务端收到请求后,会对 SQL 语句进行解析、优化和执行,最终再将执行结果封装成响应报文返回给客户端。

这是我们与 MySQL 交互最常见的一种方式。

但是除了直接使用 MySQL 客户端手动输入 SQL 语句之外,还有另一种更贴近实际业务开发的方式:通过 C/C++ 程序连接 MySQL 服务端,并在程序内部完成对数据库的访问操作。

这里需要先明确一点:MySQL 服务端本质上是一个数据库管理系统。它并不是简单地"存放数据",而是通过 SQL 层完成 SQL 语句的解析、优化和执行,再通过存储引擎层完成数据的组织、缓存、读写以及持久化。

在实际服务器开发场景中,服务器程序通常需要保存大量业务数据,例如用户注册信息、登录状态、订单信息、文章内容等。这些数据不能只存储在内存中,一方面是因为数据规模可能很大,内存无法完整容纳;另一方面更重要的是,内存中的数据不具备持久性,一旦进程退出或者机器重启,数据就会丢失。

因此,这类需要长期保存的数据通常会存储到磁盘等外存设备中。而服务器程序并不会自己直接去管理磁盘上的复杂数据结构,而是将这部分数据管理工作交给 MySQL 来完成。服务器程序只需要根据业务需求构造对应的 SQL 语句,然后将 SQL 语句发送给 MySQL 服务端执行,最终拿到 MySQL 返回的执行结果即可。

而 MySQL 虽然是数据库管理系统,但它本身也是一个网络服务进程。既然 C/C++ 程序想要与 MySQL 服务端进行交互,那么它就必须具备 MySQL 客户端的能力。

所谓客户端能力,主要包括:能够与 MySQL 服务端建立 TCP 连接,能够完成 MySQL 协议中的握手与认证过程,能够将 SQL 语句封装成符合 MySQL 协议格式的请求报文,并且能够接收和解析 MySQL 服务端返回的响应结果。

不过在实际开发中,我们并不需要自己从零实现这些 MySQL 协议细节。通常情况下,C/C++ 程序会借助 MySQL 官方提供的客户端库,例如 libmysqlclient 。这些客户端库已经帮我们封装好了连接 MySQL、发送 SQL 请求、接收查询结果等底层细节。

因此,从本质上来说,C/C++ 程序连接 MySQL,并不是让 C/C++ 程序直接操作磁盘上的数据库文件,而是让 C/C++ 程序借助 MySQL 客户端库具备访问 MySQL 服务端的能力。

当这种能力具备之后,程序与 MySQL 的交互方式就发生了变化。

以前我们是在命令行客户端中手动输入 SQL 语句,然后由 MySQL 客户端发送给服务端执行。而现在,SQL 语句不再需要程序员在命令行中一条条手动输入,而是写在 C/C++ 程序的业务逻辑中,或者根据用户请求、业务参数动态生成。

需要注意的是,SQL 语句本质上就是一段字符串。C/C++ 程序可以在内部构造出对应的 SQL 字符串,然后通过 MySQL 客户端库将其发送给 MySQL 服务端。MySQL 服务端执行完 SQL 语句后,再将执行结果返回给 C/C++ 程序,程序再根据返回结果继续完成后续的业务处理。

所以,C/C++ 程序连接 MySQL 的本质可以概括为一句话:

C/C++ 程序并不是直接管理数据库文件,而是借助 MySQL 客户端库具备 MySQL 客户端的能力,从而能够连接 MySQL 服务端、发送 SQL 请求,并接收 MySQL 返回的执行结果。

C/C++ 访问 MySQL:从客户端库引入到核心 API 调用流程

C/C++ 连接 MySQL 的前置准备:引入客户端库并完成编译链接

根据上文,我们知道,MySQL 本质上是一个典型的客户端-服务器架构的网络服务程序。既然 C/C++ 程序想要连接 MySQL 服务端,那么它就必须具备 MySQL 客户端的能力。

所谓具备 MySQL 客户端的能力,并不是说需要我们从零开始手动造轮子,自己创建 socket、实现 MySQL 协议、完成握手认证、封装请求报文、解析响应结果等一整套客户端与服务端的交互流程。

事实上,MySQL 已经将这些客户端能力封装到了对应的客户端库中。对于 C/C++ 程序来说,我们只需要调用这些库函数,就可以完成连接 MySQL 服务端、发送 SQL 请求、接收查询结果等操作。

而这些库函数通常就封装在 libmysqlclient 客户端库中。我们可以通过 MySQL 官网下载安装对应的开发库,也可以直接通过 Linux 系统中的 yum 源进行安装。

通过 yum 安装 MySQL 开发包后,系统中通常会安装两类核心内容:一类是头文件,另一类是客户端库文件。

首先是头文件。头文件主要用于在编译阶段提供函数声明,使编译器能够识别这些 MySQL 客户端库函数。通常情况下,相关头文件会被安装到 /usr/include/mysql 目录下,其中最核心的头文件就是 mysql.h。该文件中保存了大量 MySQL 客户端库函数的声明,例如后续会使用到的 mysql_initmysql_real_connectmysql_query 等函数。

但是需要注意的是,头文件中保存的只是函数声明,而不是函数真正的实现。

我们知道,一个 C/C++ 源文件最终生成可执行程序,大体需要经过编译和链接两个阶段。在编译阶段,编译器主要根据头文件中的函数声明检查函数调用是否合法,并将源文件编译生成目标文件,也就是 .o 文件。

如果我们在源文件中调用了 mysql_initmysql_real_connect 这类函数,那么编译器只需要通过 mysql.h 知道这些函数的函数名、参数列表以及返回值类型,就可以通过编译检查。

但是这些函数的真正实现并不在我们的源文件中,也不在头文件中,而是在 MySQL 客户端库文件中。因此,在目标文件中,这些函数调用会暂时以"未定义符号"的形式存在,等待链接阶段进一步处理。

到了链接阶段,链接器就需要根据我们指定的库文件去查找这些未定义符号对应的函数实现。而 MySQL 客户端库对应的动态库文件通常就是 libmysqlclient.so。在 Linux 系统中,该动态库一般会被安装到系统的动态库搜索路径下,例如 /usr/lib64/mysql 目录。

因此,在编译链接 C/C++ 程序时,除了要让编译器能够找到 mysql.h 头文件之外,还需要让链接器能够找到 libmysqlclient.so 动态库文件,并显式告诉链接器需要链接 mysqlclient 这个库。

如果系统中安装了 mysql_config 工具,那么可以直接通过下面这种方式进行编译:

bash 复制代码
g++ main.cc -o main $(mysql_config --cflags --libs)

其中,mysql_config --cflags 会给出编译阶段需要的头文件路径选项,mysql_config --libs 会给出链接阶段需要的库路径和库名选项。

如果我们手动指定,也可以写成类似下面这种形式:

bash 复制代码
g++ -I/usr/include/mysql main.cc -o main -L/usr/lib64/mysql -lmysqlclient

其中:

text 复制代码
-I/usr/include/mysql     告诉编译器去哪里查找 mysql.h 头文件
-L/usr/lib64/mysql       告诉链接器去哪里查找 libmysqlclient.so 动态库
-lmysqlclient            告诉链接器链接 mysqlclient 客户端库

这里需要注意,链接时并不需要直接写完整的 libmysqlclient.so,而是使用 -lmysqlclient。因为链接器在处理 -lxxx 选项时,会自动按照规则去查找名为 libxxx.solibxxx.a 的库文件。

所以,C/C++ 程序连接 MySQL 的准备工作,本质上可以概括为两点:

第一,在源文件中包含 mysql.h 头文件,让编译器能够识别 MySQL 客户端库函数的声明;

第二,在链接阶段链接 libmysqlclient 客户端库,让链接器能够找到这些库函数真正的实现。

当这两步完成之后,C/C++ 程序就可以借助 MySQL 客户端库具备 MySQL 客户端的能力,从而连接 MySQL 服务端、发送 SQL 请求,并接收 MySQL 服务端返回的执行结果。


认识 MYSQL 对象:客户端连接上下文的抽象描述

接下来,在知道如何让我们编写的 C/C++ 程序链接到第三方库之后,我们就可以正式开始认识 MySQL 客户端库提供的相关函数。

根据上文,我们已经知道,所谓 MySQL 客户端库,本质上就是将客户端与服务端之间的交互流程封装成了一组 API。对于 C/C++ 程序来说,我们并不需要自己从零实现 MySQL 协议,也不需要自己手动完成建立连接、握手认证、请求封装、响应解析等底层细节,只需要调用客户端库提供的相关函数,就可以完成与 MySQL 服务端之间的交互。

不过,在真正调用这些库函数之前,我们首先需要认识一个非常核心的对象:MYSQL

我们知道,对于 MySQL 服务端来说,它需要同时维护大量客户端连接。既然存在大量连接,那么 MySQL 服务端就需要采用"先描述,再组织"的方式来管理这些连接。也就是说,MySQL 服务端会为每一个客户端会话维护一个对应的上下文对象,例如服务端内部的 THD 对象。该对象中保存了当前连接相关的上下文信息,例如连接状态、执行状态、权限信息、事务状态等内容。

类似地,从 C/C++ 程序这一侧来看,一个程序也可能与 MySQL 服务端建立多个连接。比如,一个 C/C++ 程序可以连接同一个 MySQL 服务端中的不同数据库,也可以连接不同的 MySQL 服务端实例。因此,客户端程序这一侧同样需要一个对象来描述某一次具体的 MySQL 连接。

这个对象不需要我们自己定义,MySQL 客户端库已经帮我们封装好了,它就是 MYSQL 类型的对象。

MYSQL 本质上可以理解为 MySQL 客户端库提供的一个连接句柄。它描述的是 C/C++ 程序与 MySQL 服务端之间的一次连接上下文,而不是数据库本身。这个对象内部会保存或关联当前连接所需要的一些上下文信息,例如服务端地址、端口号、用户认证信息、当前默认数据库、连接状态、错误码、错误信息、字符集信息以及网络通信相关状态等。

text 复制代码
服务端地址
端口号
用户名/认证信息
当前连接状态
错误码和错误信息
当前默认数据库
字符集信息
网络通信相关状态
查询执行结果相关状态

因此,后续无论是初始化连接、真正连接 MySQL 服务端、发送 SQL 语句、获取执行结果,还是最终关闭连接,基本都要围绕这个 MYSQL 对象展开。

这背后其实体现的是一种非常常见的系统设计思想:先描述,再组织。

也就是说,当系统中存在某种复杂实体或者复杂关系时,通常不会把相关状态零散地放在各个地方,而是会先定义一个对象来描述它,然后后续所有操作都围绕这个对象展开。

这个思想在其他地方也非常常见。

比如在 Linux 网络编程中,我们在用户层拿到的 sockfd 本质上就是一个文件描述符。它本身只是用户层操作网络连接的入口,而真正的连接状态、协议状态、收发缓冲区等信息,则由内核中的 socketsock 等对象维护。

再比如我们自己实现 HTTP 服务器时,也通常会封装一个服务器类或者服务器实例。这个对象中可能会保存监听套接字对应的文件描述符、绑定的 IP 地址、端口号、epoll 对象、线程池、定时器以及连接管理结构等内容。这样一来,服务器运行所需要的状态和接口就被统一收拢到了一个对象中,后续启动服务器、接收连接、处理事件等操作,也都可以围绕这个服务器对象展开。

因此,MySQL 客户端库中的 MYSQL 对象也可以按照类似的方式理解:

MYSQL 对象就是客户端层面对一次 MySQL 连接的抽象描述。它保存的是客户端程序与 MySQL 服务端之间这一次连接所需要的上下文信息,后续所有连接、查询、获取结果以及关闭连接等操作,都是围绕这个连接句柄展开的。


MySQL 客户端库核心函数调用流程详解

MySQL 客户端库的基本调用流程

认识了 MYSQL 对象之后,接下来我们便正式进入客户端与服务端交互的具体流程。

在具体讲解客户端库函数之前,我们首先需要明确一点:客户端与服务端之间的交互流程,本质上是一套相对固定的模板。有过网络编程经验的读者应该知道,一个客户端想要与服务端通信,大致流程一般是:先建立连接,然后发送请求报文,服务端接收并解析请求报文,执行对应的业务逻辑,最后再将处理结果通过响应报文返回给客户端。

MySQL 客户端与 MySQL 服务端之间的交互,本质上也是类似的。只不过这里发送的请求内容主要是 SQL 语句,而服务端收到 SQL 语句之后,会对其进行解析、优化和执行,最终再将执行结果返回给客户端程序。

因此,MySQL 客户端库的使用,并不是零散地记忆一堆函数,而是要把它理解成一套固定的客户端交互流程。这个流程大致可以概括为:

text 复制代码
初始化 MySQL 连接句柄
        ↓
连接 MySQL 服务端
        ↓
发送 SQL 请求
        ↓
获取并处理执行结果
        ↓
关闭连接,释放资源

对应到 MySQL 客户端库中,常见的函数调用流程大致如下:

cpp 复制代码
mysql_init()
    ↓
mysql_real_connect()
    ↓
mysql_query() / mysql_real_query()
    ↓
mysql_store_result() / mysql_use_result()
    ↓
mysql_fetch_row()
    ↓
mysql_free_result()
    ↓
mysql_close()

这和网络编程中的客户端流程其实非常相似。网络编程中,客户端通常是先通过 socket() 创建套接字,再通过 connect() 连接服务端,随后通过 read() / write() 完成数据收发,最后通过 close() 关闭连接。

而在 MySQL 客户端库中,这套过程被封装成了对应的库函数。我们后续要做的,就是按照这条交互流程,依次理解每个核心函数的作用、调用时机以及其背后的基本行为。

mysql_init:初始化客户端连接句柄

整个流程的起点,就是初始化一个 MYSQL 对象。

前面我们已经知道,MYSQL 对象本质上是 MySQL 客户端库提供的连接句柄,用来描述 C/C++ 程序与 MySQL 服务端之间的一次连接上下文。后续无论是连接服务端、发送 SQL、获取错误信息,还是关闭连接,基本都要围绕这个 MYSQL 对象展开。

初始化 MYSQL 对象需要调用 mysql_init 函数,其函数原型大致如下:

cpp 复制代码
MYSQL *mysql_init(MYSQL *mysql);

这个函数的作用,就是初始化一个 MYSQL 连接句柄。它的参数可以传入 NULL,也可以传入一个已经存在的 MYSQL 对象地址。

最常见的写法是:

cpp 复制代码
MYSQL* mysql = mysql_init(NULL);

这里传入 NULL,表示我们不提前创建 MYSQL 对象,而是让 MySQL 客户端库内部帮我们申请并初始化一个 MYSQL 对象。如果初始化成功,函数会返回一个 MYSQL* 指针;如果初始化失败,则返回 NULL

因此,调用 mysql_init 之后,需要立刻对返回值进行校验:

cpp 复制代码
MYSQL* mysql = mysql_init(NULL);
if (mysql == NULL)
{
    std::cerr << "mysql_init failed" << std::endl;
    return 1;
}

除了传入 NULL 之外,我们也可以自己提前定义一个 MYSQL 对象,然后将它的地址传给 mysql_init

cpp 复制代码
MYSQL mysql;
MYSQL* ret = mysql_init(&mysql);

这种写法表示:MYSQL 对象本身的空间由我们自己提供,通常是在栈上开辟;而 mysql_init 只负责初始化这块对象空间中的内部状态。

也就是说:

cpp 复制代码
MYSQL mysql;              
// 在栈上开辟 MYSQL 对象空间

mysql_init(&mysql);       
// 初始化该对象内部状态

这种情况下,后续如果调用:

cpp 复制代码
mysql_close(&mysql);

mysql_close 只会清理该 MYSQL 对象内部关联的资源,例如连接状态、网络资源、缓冲区等,并不会释放 mysql 这个栈对象本身的空间。因为这个对象本身不是客户端库动态申请出来的,而是我们自己在栈上创建的,它的空间会在函数退出时由系统自动回收。

而如果使用的是:

cpp 复制代码
MYSQL* mysql = mysql_init(NULL);

那么 MYSQL 对象本身的空间就是由 MySQL 客户端库内部申请的。此时后续调用:

cpp 复制代码
mysql_close(mysql);

不仅会清理连接内部关联的资源,也会释放这个由客户端库内部申请出来的 MYSQL 对象空间。

所以,mysql_init 参数是否为 NULL,本质区别在于:MYSQL 对象本身的空间到底由谁提供。

可以总结为:

text 复制代码
mysql_init(NULL)
    → 客户端库内部申请 MYSQL 对象空间,并完成初始化
    → mysql_close() 清理内部资源,并释放 MYSQL 对象本身

mysql_init(&mysql)
    → 用户自己提供 MYSQL 对象空间,mysql_init 只负责初始化
    → mysql_close() 只清理内部资源,不释放 mysql 这个对象本身

在实际开发中,更常见、更简单的写法是直接传入 NULL

cpp 复制代码
MYSQL* mysql = mysql_init(NULL);

这种方式由客户端库统一完成 MYSQL 对象的申请、初始化和最终释放,使用起来更加方便,也不容易在对象生命周期管理上出错。

因此,mysql_init 可以理解为整个 MySQL 客户端交互流程的起点。只有先成功初始化出一个 MYSQL 连接句柄,后续才能基于这个句柄继续调用 mysql_real_connect 连接 MySQL 服务端,并进一步完成 SQL 请求发送和结果处理等操作。


错误处理:通过 mysql_errnomysql_error 定位失败原因

其次,前面我们已经知道,MYSQL 对象本质上是 MySQL 客户端库提供的连接句柄。它不仅保存了当前连接的上下文信息,也会维护该连接最近一次客户端库函数调用相关的错误状态。

因此,在使用 MySQL 客户端库函数时,我们不能默认每一步调用都会成功,而是需要根据每个函数的返回值判断其是否调用成功。只有当前一步执行成功之后,才应该继续执行后续流程;如果当前函数调用失败,就需要及时获取错误信息并进行处理。

当然,如果只是简单调试,我们可以自己打印一段固定的错误提示。但是这种方式只能说明"某个函数调用失败了",并不能告诉我们具体失败原因。

而 MySQL 客户端库已经为我们提供了对应的错误信息获取接口。常用的两个函数分别是:

cpp 复制代码
mysql_errno(mysql);  // 获取最近一次客户端库函数调用失败对应的错误码
mysql_error(mysql);  // 获取最近一次客户端库函数调用失败对应的错误描述字符串

其中,mysql_errno 用来获取错误码,返回的是一个整数类型的错误编号;而 mysql_error 用来获取错误描述,返回的是一个字符串,用来说明具体的错误原因。

例如:

cpp 复制代码
if (mysql_query(mysql, sql.c_str()) != 0)
{
    std::cerr << "mysql_query failed, errno: "
              << mysql_errno(mysql)
              << ", error: "
              << mysql_error(mysql)
              << std::endl;
}

这里需要注意的是,只有当返回值表示调用失败时,我们才进一步调用 mysql_errnomysql_error 获取详细的错误信息。

所以,这里的基本使用逻辑可以概括为:

text 复制代码
先检查函数返回值
        ↓
如果返回值表示失败
        ↓
调用 mysql_errno 获取错误码
调用 mysql_error 获取错误描述
        ↓
根据错误信息定位失败原因

因此,MYSQL 对象不仅是一次 MySQL 连接的上下文对象,也可以看作是客户端库记录当前连接错误状态的位置。当某个客户端库函数调用失败时,我们就可以围绕这个 MYSQL 连接句柄获取对应的错误码和错误描述,从而更加准确地定位问题。


mysql_real_connect:连接 MySQL 服务端实例并选择默认数据库

创建并初始化好 MYSQL 对象之后,按照客户端与服务端交互的固定流程,下一步就需要真正与 MySQL 服务端建立连接。而完成这个动作所需要调用的核心函数,就是 mysql_real_connect

在正式分析连接过程之前,我们先来看一下 mysql_real_connect 函数的基本参数。其函数原型大致如下:

cpp 复制代码
MYSQL *mysql_real_connect(
    MYSQL *mysql,
    const char *host,
    const char *user,
    const char *passwd,
    const char *db,
    unsigned int port,
    const char *unix_socket,
    unsigned long client_flag
);

其中,第一个参数 mysql 就是前面通过 mysql_init 初始化得到的 MYSQL 连接句柄。后续建立连接、发送 SQL、获取错误信息以及关闭连接等操作,基本都要围绕这个连接句柄展开。

第二个参数 host 表示 MySQL 服务端所在的主机地址,也就是客户端程序要连接哪个 MySQL 服务端实例。它可以是 IP 地址,例如 "127.0.0.1",也可以是主机名。需要注意的是,如果这里传入的是 "localhost",在 Linux 环境下通常会优先尝试使用 Unix 域套接字连接;如果希望明确通过 TCP 连接本机 MySQL 服务端,可以使用 "127.0.0.1"

第三个参数 user 表示连接 MySQL 服务端所使用的用户名。MySQL 在认证时并不是只看用户名本身,而是会结合"用户名 + 主机来源"这个二元组来识别账户,例如 'wz'@'localhost''wz'@'%' 本质上是不同的账户。

第四个参数 passwd 表示该用户对应的密码。客户端库会基于 MySQL 协议完成认证过程,我们不需要自己手动加密或封装密码认证报文。

第五个参数 db 表示连接成功后要设置的默认数据库。

第六个参数 port 表示 MySQL 服务端监听的端口号。MySQL 默认端口通常是 3306。如果该参数传入 0,则表示使用默认端口。

第七个参数 unix_socket 用于指定 Unix 域套接字文件路径。这个参数主要用于本机进程之间通过 Unix 域套接字连接 MySQL 服务端。如果我们是通过 TCP 方式连接 MySQL,一般直接传入 NULL 即可。

第八个参数 client_flag 用于设置客户端连接的一些额外选项,例如是否启用压缩协议、是否允许多语句执行等。对于普通连接场景,一般传入 0 即可。

因此,一个常见的连接写法如下:

cpp 复制代码
MYSQL* mysql = mysql_init(NULL);
if (mysql == NULL)
{
    std::cerr << "mysql_init failed" << std::endl;
    return 1;
}

MYSQL* ret = mysql_real_connect(
    mysql,
    "127.0.0.1",
    "wz",
    "123456",
    "wz_db",
    3306,
    NULL,
    0
);

if (ret == NULL)
{
    std::cerr << "mysql_real_connect failed: "
              << mysql_error(mysql)
              << std::endl;
    return 2;
}

这里需要注意,mysql_real_connect 调用成功后,会返回一个 MYSQL* 指针,通常就是传入的那个 mysql 连接句柄;如果连接失败,则返回 NULL。因此,调用该函数之后,同样需要立刻检查返回值,并在失败时通过 mysql_error 获取具体的错误信息。

总结一下,mysql_real_connect 的参数本质上描述了这几个问题:

text 复制代码
连接句柄:用哪个 MYSQL 对象描述这次连接
服务端位置:连接哪个 host 和 port
用户身份:使用哪个 user 和 passwd 进行认证
默认数据库:连接成功后默认使用哪个 db
连接方式:使用 TCP 还是 Unix 域套接字
连接选项:是否启用额外的客户端能力

这里需要先明确一个容易混淆的点:mysql_real_connect 真正连接的对象并不是某一个具体的数据库,而是 MySQL 服务端实例,也就是正在运行的 mysqld 服务进程。

从网络层面来看,客户端能够连接的目标只能是某个服务端的 IP 地址和端口号。例如 MySQL 默认监听的端口是 3306,所以 C/C++ 程序本质上是向 MySQL 服务端监听的 IP:Port 发起连接请求。这个端口背后对应的是 MySQL 服务端实例,而不是某一个具体的数据库。

因此,数据库并不是网络连接的目标。数据库只是 MySQL 服务端实例内部的一个逻辑命名空间,用来组织表、视图、存储过程等数据库对象。

从底层流程来看,mysql_real_connect 的连接过程大致可以分为几个阶段。

首先是 TCP 层面的连接。客户端会向 MySQL 服务端监听的 IP 地址和端口号发起 TCP 连接请求,经过三次握手之后,客户端与 MySQL 服务端之间就建立起了一条 TCP 连接。

TCP 连接建立成功之后,接下来才会进入 MySQL 协议层面的握手与认证阶段。在这个阶段中,客户端会按照 MySQL 协议向服务端提交用户名、密码等认证信息。MySQL 服务端收到认证信息之后,会根据"用户名 + 主机来源"这个二元组识别对应账户,并校验该用户是否已经注册、密码是否匹配。

如果用户不存在,或者密码不正确,那么认证阶段就会失败,mysql_real_connect 会返回 NULL。此时我们可以通过 mysql_error 获取具体的错误原因。

认证通过之后,还需要注意 mysql_real_connect 中的默认数据库参数。这个参数并不表示"连接到某个数据库",而是表示:在连接 MySQL 服务端实例成功之后,将指定的数据库设置为当前连接的默认数据库。

也就是说,如果我们在调用 mysql_real_connect 时传入了默认数据库名,那么 MySQL 会尝试将该数据库设置为当前连接的默认数据库。这个过程可以理解为效果上等价于连接成功后自动执行了一次:

sql 复制代码
USE 数据库名;

因此,如果指定了默认数据库,就必须保证该数据库已经存在,并且当前用户拥有访问该数据库的权限。否则,即使前面的 TCP 连接和用户认证都没有问题,连接过程仍然可能失败,mysql_real_connect 最终返回 NULL

比如,如果数据库不存在,可能会出现类似错误:

text 复制代码
Unknown database 'xxx'

如果用户没有访问该数据库的权限,可能会出现类似错误:

text 复制代码
Access denied for user 'xxx'@'xxx' to database 'xxx'

而如果调用 mysql_real_connect 时没有指定默认数据库,也就是将数据库参数传入 NULL,那么连接仍然可以成功。此时只是表示当前连接暂时没有默认数据库。后续如果想要操作某个数据库,就需要再通过其他方式明确指定。

例如,可以手动发送 SQL 语句:

sql 复制代码
USE wz_db;

也可以调用客户端库提供的函数:

cpp 复制代码
mysql_select_db(mysql, "wz_db");

还可以在 SQL 语句中直接使用完整的库名和表名:

sql 复制代码
SELECT * FROM wz_db.student;

所以这里需要注意:不指定默认数据库并不会导致连接失败,只是后续执行 SQL 时需要明确告诉 MySQL 当前要操作哪个数据库。 如果直接执行不带库名的表操作,例如:

sql 复制代码
SELECT * FROM student;

而当前连接又没有默认数据库,就可能出现:

text 复制代码
No database selected

因此,在调用 mysql_real_connect 之前,最好提前确认几个条件:

text 复制代码
MySQL 服务端正在运行,并且监听的 IP 和端口正确
用户已经在 MySQL 授权系统中注册
用户名、主机来源和密码能够匹配
如果指定默认数据库,该数据库必须已经存在
当前用户需要拥有访问该数据库的相应权限

所以,mysql_real_connect 的核心作用可以概括为:

它负责让 C/C++ 程序连接到 MySQL 服务端实例,并完成 TCP 连接、MySQL 协议握手、用户认证以及可选的默认数据库选择。

这里真正连接的是 MySQL 服务端实例,而默认数据库参数只是用来指定当前连接后续默认操作的数据库。这个区别一定要理解清楚,否则很容易误以为 C/C++ 程序是直接"连接到某个数据库"。


mysql_query:发送 SQL 请求并区分查询结果处理方式

与 MySQL 服务端建立连接成功之后,接下来客户端程序就可以开始向服务端发送 SQL 请求了。

前面我们已经知道,SQL 语句本质上就是一段字符串。因此在 C/C++ 程序中,我们可以根据业务需求构造对应的 SQL 字符串,然后将这段 SQL 字符串交给 MySQL 客户端库,由客户端库发送给 MySQL 服务端执行。

这个过程所使用的核心函数就是 mysql_query,其函数原型大致如下:

cpp 复制代码
int mysql_query(MYSQL *mysql, const char *stmt_str);

其中,第一个参数 mysql 表示当前连接句柄,也就是前面通过 mysql_init 初始化,并通过 mysql_real_connect 成功连接 MySQL 服务端之后得到的 MYSQL 对象。

第二个参数 stmt_str 表示要发送给 MySQL 服务端执行的 SQL 语句字符串。比如:

cpp 复制代码
std::string sql = "insert into student values(1, 'wz', 20)";
int ret = mysql_query(mysql, sql.c_str());

这里的含义就是:通过当前 MYSQL 连接句柄,将 sql.c_str() 指向的 SQL 字符串发送给 MySQL 服务端,由服务端完成 SQL 语句的解析、优化和执行。

mysql_query 的返回值也非常重要。如果返回值为 0,表示 SQL 语句执行成功;如果返回值非 0,则表示执行失败。执行失败时,可以通过 mysql_error 获取具体的错误原因:

cpp 复制代码
if (mysql_query(mysql, sql.c_str()) != 0)
{
    std::cerr << "mysql_query failed: "
              << mysql_error(mysql)
              << std::endl;
}

这里需要注意,mysql_query 接收的是一个以 \0 结尾的 C 风格字符串。因此在使用 std::string 构造 SQL 语句时,通常需要通过 c_str() 将其转换成 const char* 类型。

如果 SQL 字符串中可能包含二进制数据,尤其是包含 \0 字节,那么就不适合使用 mysql_query,而应该使用 mysql_real_query。因为 mysql_real_query 可以显式指定 SQL 字符串的长度,避免因为中间出现 \0 而导致字符串被提前截断。

发送 SQL 请求之后,客户端程序还需要根据 SQL 类型决定后续如何处理执行结果。

我们知道,对数据库表的基本操作主要是 CRUD,也就是:

text 复制代码
INSERT  插入数据
DELETE  删除数据
UPDATE  修改数据
SELECT  查询数据

对于 INSERTDELETEUPDATE 这类非查询语句来说,客户端通常更关心两个问题:

text 复制代码
第一,SQL 语句是否执行成功;
第二,本次操作影响了多少行数据。

因此,这类语句在调用 mysql_query 之后,一般只需要先检查返回值。如果返回值为 0,说明语句执行成功;如果返回值非 0,说明语句执行失败。

执行成功之后,如果还想知道本次操作影响了多少行数据,可以调用 mysql_affected_rows

cpp 复制代码
std::string sql = "delete from student where id = 1";

if (mysql_query(mysql, sql.c_str()) != 0)
{
    std::cerr << "delete failed: "
              << mysql_error(mysql)
              << std::endl;
}
else
{
    std::cout << "affected rows: "
              << mysql_affected_rows(mysql)
              << std::endl;
}

但是 SELECT 查询语句就不一样了。SELECT 的核心目的不是修改数据,而是从数据库中查询数据。服务端执行完 SELECT 之后,返回的并不是简单的成功或失败状态,而是一个结果集。

这个结果集可以理解为一张临时的二维表,其中包含多行、多列数据。客户端程序后续往往还需要对这个结果集进行遍历,比如一行一行读取查询结果,再将每一列的数据取出来进行业务处理。

因此,对于 SELECT 语句来说,调用 mysql_query 只是完成了 SQL 请求的发送和服务端执行。真正想要拿到查询结果,还需要继续调用结果集相关的接口,例如:

cpp 复制代码
MYSQL_RES* res = mysql_store_result(mysql);

该函数会将查询结果保存到客户端这一侧,并返回一个 MYSQL_RES* 类型的结果集对象。

mysql_store_result:获取并保存 SELECT 查询结果集

根据上文,我们已经知道,如果发送给 MySQL 服务端的是一条 SELECT 查询语句,那么服务端执行完成之后,返回的并不是简单的成功或失败状态,而是一个查询结果集。

这个结果集可以理解为一张临时的二维表结构,其中包含多行、多列数据。客户端程序后续通常还需要对这个结果集进行访问,比如逐行读取查询结果,并取出每一行中的各个字段值进行业务处理。

这里需要注意,SELECT 查询得到的结果集一定是通过当前这条 MYSQL 连接返回的。也就是说,服务端执行完 SQL 语句之后,会将查询结果通过当前连接对应的网络通道返回给客户端,MYSQL 对象作为连接句柄,会维护这次通信过程中的连接状态、协议状态以及结果读取相关的状态。

但是,结果集最终并不是让我们直接围绕 MYSQL 对象进行遍历。因为 MYSQL 对象的核心职责是描述一次客户端与 MySQL 服务端之间的连接上下文,而结果集本身则是一份独立的查询结果数据。如果继续让 MYSQL 对象直接承担结果集遍历的职责,那么连接管理和结果集访问就会混在一起。

也就是说,在执行完 SELECT 语句之后,客户端库会通过当前 MYSQL 连接读取服务端返回的结果,然后将完整的查询结果组织并保存到一个 MYSQL_RES 对象中。后续我们遍历结果集时,主要围绕的就不再是 MYSQL 连接句柄,而是这个 MYSQL_RES 结果集对象,不会继续占用 MYSQL 连接句柄。。

这个过程需要调用的函数就是 mysql_store_result,其函数原型大致如下:

cpp 复制代码
MYSQL_RES *mysql_store_result(MYSQL *mysql);

其中,参数 mysql 表示当前连接句柄,也就是刚刚执行过 SELECT 查询的那个 MYSQL 对象。

调用该函数之后,客户端库会从当前连接中读取最近一次查询返回的完整结果集,并在客户端内存中创建一个 MYSQL_RES 对象来保存这份结果集。如果获取成功,函数会返回一个 MYSQL_RES* 指针;如果获取失败,则返回 NULL

例如:

cpp 复制代码
if (mysql_query(mysql, "select * from student") != 0)
{
    std::cerr << "select failed: "
              << mysql_error(mysql)
              << std::endl;
    return 1;
}

MYSQL_RES* res = mysql_store_result(mysql);
if (res == NULL)
{
    std::cerr << "mysql_store_result failed: "
              << mysql_error(mysql)
              << std::endl;
    return 2;
}

这里的逻辑可以理解为:

text 复制代码
mysql_query(mysql, sql)
        ↓
通过 MYSQL 连接句柄向服务端发送 SELECT 语句
        ↓
服务端执行 SELECT,并通过当前连接返回结果集
        ↓
mysql_store_result(mysql)
        ↓
客户端库从当前连接中读取完整结果集
        ↓
将结果集保存到 MYSQL_RES 对象中

所以,MYSQLMYSQL_RES 的职责可以这样区分:

text 复制代码
MYSQL
    描述客户端与 MySQL 服务端之间的一次连接上下文;
    负责连接、发送 SQL、接收响应以及维护连接状态。

MYSQL_RES
    描述一次 SELECT 查询返回的结果集;
    负责保存和组织查询结果,供客户端后续遍历访问。

这其实也是一种解耦思想:连接对象负责连接和通信,结果集对象负责保存和访问查询结果。这样一来,查询结果被从连接中取出之后,就可以通过独立的 MYSQL_RES 对象进行遍历,而不是继续把所有操作都压在 MYSQL 对象上。

当然,这里我们讲的是 mysql_store_result。它的特点是会一次性将完整结果集读取到客户端内存中,因此后续遍历 MYSQL_RES 时,主要是在客户端本地访问这份已经保存好的结果集。

MySQL 客户端库中还有另一个函数叫 mysql_use_result。它和 mysql_store_result 的区别在于:mysql_use_result 不会一次性把完整结果集全部读取到客户端,而是后续调用 mysql_fetch_row 时再逐行读取。这样做可以减少客户端内存压力,但是在结果集读取完成之前,这条连接会被当前结果集占用,不能随意继续执行下一条 SQL。

所以在普通场景下,尤其是结果集规模不大的时候,我们一般优先使用 mysql_store_result,因为它更直观,也更适合入门理解。

因此,对于 SELECT 查询语句来说,完整流程可以概括为:

text 复制代码
先通过 mysql_query 发送 SELECT 语句
        ↓
再通过 mysql_store_result 获取完整结果集
        ↓
结果集被保存到 MYSQL_RES 对象中
        ↓
后续通过结果集相关函数逐行遍历
        ↓
使用完成后释放 MYSQL_RES 结果集对象

也就是说,SELECT 查询和 INSERTDELETEUPDATE 这类非查询语句最大的区别就在于:

非查询语句通常只需要判断执行是否成功以及影响了多少行;而 SELECT 查询语句除了要判断执行是否成功之外,还需要进一步获取服务端返回的结果集,并通过 MYSQL_RES 对象对结果集进行遍历处理。

mysql_fetch_row:逐行遍历 SELECT 查询结果集

根据上文,我们已经通过 mysql_store_result 成功获取到了 SELECT 查询返回的结果集。接下来要做的事情,就是对这个结果集进行遍历,从中一行一行取出查询到的数据。

我们知道,SELECT 查询得到的结果集,在逻辑上可以理解为一张临时的二维表结构。它由多行多列组成,每一行表示一条记录,每一列表示该记录中的一个字段值。

不过需要注意的是,在 MySQL C API 中,我们并不是直接拿到一个普通的二维数组去随机访问结果集,而是通过客户端库提供的函数逐行读取结果。

在客户端库中,整个结果集由 MYSQL_RES 对象描述,而结果集中的每一行则由 MYSQL_ROW 类型描述。
MYSQL_ROW 在类型定义上就是 char**,也就是一个字符串指针数组。它表示结果集中的一行数据,其中每一个 char* 元素指向当前行中某一列的字段值。

也就是说,对于一行查询结果来说,可以这样理解:

text 复制代码
MYSQL_ROW row
        ↓
row[0]  指向当前行第 1 列的字段值
row[1]  指向当前行第 2 列的字段值
row[2]  指向当前行第 3 列的字段值
...

通过 mysql_fetch_row 获取一行结果后,当前行中的每一列字段值在 C API 层面都以 char* 的形式返回。即使数据库中某列原本是 INTDATE 等类型,客户端拿到后通常也是对应的字符串表示;如果后续需要按照整数等类型处理,就需要程序员自行进行类型转换。

结果集遍历所使用的核心函数是 mysql_fetch_row,其函数原型大致如下:

cpp 复制代码
MYSQL_ROW mysql_fetch_row(MYSQL_RES *result);

该函数的作用是:从 MYSQL_RES 结果集对象中取出下一行数据。每调用一次,内部的读取位置就会向后移动一行,并返回当前这一行对应的 MYSQL_ROW。当结果集中的所有行都被读取完之后,该函数会返回 NULL

因此,遍历结果集时,通常不是先获取行数再用下标访问每一行,而是不断调用 mysql_fetch_row,直到其返回 NULL 为止。

不过,在遍历每一行内部的列值时,我们需要知道当前结果集一共有多少列。这个列数可以通过 mysql_num_fields 获取:

cpp 复制代码
unsigned int fields_num = mysql_num_fields(res);

其中,res 就是前面通过 mysql_store_result 得到的 MYSQL_RES* 结果集对象。

于是,一个典型的结果集遍历流程如下:

cpp 复制代码
MYSQL_RES* res = mysql_store_result(mysql);
if (res == NULL)
{
    std::cerr << "mysql_store_result failed: "
              << mysql_error(mysql)
              << std::endl;
    return 1;
}

unsigned int fields_num = mysql_num_fields(res);

MYSQL_ROW row;
while ((row = mysql_fetch_row(res)) != NULL)
{
    for (unsigned int i = 0; i < fields_num; ++i)
    {
        std::cout << (row[i] ? row[i] : "NULL") << "\t";
    }
    std::cout << std::endl;
}

mysql_free_result(res);

这里的遍历逻辑可以分成两层:

text 复制代码
外层 while 循环:
    不断调用 mysql_fetch_row,从结果集中一行一行取出数据。

内层 for 循环:
    根据列数 fields_num,遍历当前行中的每一个字段值。

这里还需要注意一个细节:如果数据库中某一列的值是 SQL 层面的 NULL,那么在 C API 中,对应的 row[i] 并不是字符串 "NULL",而是一个真正的空指针 NULL。因此在打印或者访问字段值之前,最好先判断 row[i] 是否为空,避免直接解引用空指针。

例如:

cpp 复制代码
std::cout << (row[i] ? row[i] : "NULL");

除了列数之外,如果使用的是 mysql_store_result,由于完整结果集已经被保存到了客户端内存中,也可以通过 mysql_num_rows 获取结果集的总行数:

cpp 复制代码
my_ulonglong rows = mysql_num_rows(res);

但是在实际遍历时,行数通常不是必须条件。更常见的方式还是通过 mysql_fetch_row 逐行读取,直到返回 NULL,这更符合结果集遍历的使用方式。

所以,结果集遍历的核心可以概括为:

text 复制代码
MYSQL_RES  描述整个结果集
MYSQL_ROW  描述结果集中的一行
row[i]     表示当前行第 i 列的字段值

也就是说,MYSQL_RES 负责保存和组织整个查询结果,mysql_fetch_row 负责从结果集中逐行取出数据,而 MYSQL_ROW 则负责表示当前取出的这一行。后续我们只需要根据列数遍历 MYSQL_ROW 中的各个字段值,就可以完成对整个查询结果集的访问。


mysql_fetch_fields:获取结果集的列元数据

除了逐行读取结果集中的数据之外,有时候我们还需要获取结果集中每一列的元数据信息。所谓列元数据,就是用来描述某一列字段属性的信息,例如列名、所属表、字段类型、字段长度、是否允许 NULL 等。

在 MySQL C API 中,结果集中每一列的元数据信息由 MYSQL_FIELD 结构体描述。也就是说,MYSQL_FIELD 并不是用来保存某一行中的字段值,而是用来描述结果集中某一列的属性信息。

c 复制代码
typedef struct st_mysql_field
{
    char *name;        // 结果集中显示的列名;如果使用 AS 别名,则这里是别名
    char *org_name;    // 原始列名;如果该列是表达式,则通常为空字符串

    char *table;       // 结果集中显示的表名;如果使用表别名,则这里是表别名
    char *org_table;   // 原始表名

    char *db;          // 该字段所属的数据库名
    char *catalog;     // catalog 名,一般为 "def"
    char *def;         // 字段默认值,通常只有 mysql_list_fields() 等场景下才设置

    unsigned long length;      // 字段的最大显示宽度
    unsigned long max_length;  // 当前结果集中该列实际出现过的最大长度

    unsigned int name_length;
    unsigned int org_name_length;
    unsigned int table_length;
    unsigned int org_table_length;
    unsigned int db_length;
    unsigned int catalog_length;
    unsigned int def_length;

    unsigned int flags;        // 字段属性标志,例如 NOT NULL、主键、自增等
    unsigned int decimals;     // 数值字段的小数位数,或时间字段的小数秒精度
    unsigned int charsetnr;    // 字符集 / 排序规则编号

    enum enum_field_types type; // 字段类型,例如 MYSQL_TYPE_LONG、MYSQL_TYPE_STRING 等

} MYSQL_FIELD;

MYSQL_FIELD 本质上就是一个用来描述结果集中某一列元数据的结构体。它不保存某一行的具体字段值,而是保存这一列的属性信息,例如列名、原始列名、所属表、字段类型、字段长度、字段属性标志等。

其中比较常用的字段有:

text 复制代码
name        结果集中显示的列名
org_name    原始表中的列名
table       结果集中显示的表名
org_table   原始表名
db          所属数据库名
length      字段最大显示宽度
max_length  当前结果集中该列实际值的最大长度
flags       字段属性标志
type        字段类型

所以这里要区分:

text 复制代码
MYSQL_FIELD  描述结果集中某一列的属性信息
MYSQL_ROW    保存当前行中每一列的具体值

如果想要获取这些列元数据,可以调用 mysql_fetch_fields 函数:

cpp 复制代码
MYSQL_FIELD* mysql_fetch_fields(MYSQL_RES *result);

该函数会返回一个 MYSQL_FIELD 结构体数组,其中每一个 MYSQL_FIELD 对象描述结果集中的一列。数组中元素的个数,正好对应结果集的列数,因此通常会配合 mysql_num_fields 一起使用:

cpp 复制代码
unsigned int fields_num = mysql_num_fields(res);
MYSQL_FIELD* fields = mysql_fetch_fields(res);

此时可以这样理解:

text 复制代码
fields[i]  描述结果集中第 i 列的元数据信息
row[i]     表示当前行第 i 列的字段值

例如,我们可以先通过 fields[i].name 打印结果集的表头,再通过 row[i] 打印每一行对应的字段值:

cpp 复制代码
unsigned int fields_num = mysql_num_fields(res);
MYSQL_FIELD* fields = mysql_fetch_fields(res);

// 打印列名
for (unsigned int i = 0; i < fields_num; ++i)
{
    std::cout << fields[i].name << "\t";
}
std::cout << std::endl;

// 打印每一行数据
MYSQL_ROW row;
while ((row = mysql_fetch_row(res)) != NULL)
{
    for (unsigned int i = 0; i < fields_num; ++i)
    {
        std::cout << (row[i] ? row[i] : "NULL") << "\t";
    }
    std::cout << std::endl;
}

这里需要注意,MYSQL_FIELD 描述的是结果集中某一列的元数据 ,而不一定等同于原始表中某一列的完整定义。因为 SELECT 查询可以使用别名、表达式、函数以及多表连接等操作,最终返回的是一个结果集,而不是简单地把原始表结构原封不动返回。

例如:

sql 复制代码
SELECT id AS student_id, name FROM student;

此时结果集中的第一列名是 student_id,而原始表中的列名是 id。在这种情况下,MYSQL_FIELD 中的 name 表示结果集中的列名,而 org_name 才表示原始列名。

所以,MYSQL_FIELD 更准确的理解是:

它用来描述 SELECT 结果集中每一列的字段元数据;而 MYSQL_ROW 则用来保存当前行中每一列的字段值。

也就是说:

text 复制代码
MYSQL_RES    描述整个结果集
MYSQL_FIELD  描述结果集中的一列元数据
MYSQL_ROW    描述结果集中的一行数据
row[i]       当前行第 i 列的字段值
fields[i]    第 i 列的字段属性信息

mysql_free_resultmysql_close:释放结果集并关闭连接

当客户端程序与 MySQL 服务端的交互结束之后,最后一个流程就是进行资源清理。

在前面的流程中,如果我们执行的是 SELECT 语句,并且通过 mysql_store_result 获取到了结果集,那么客户端库会在客户端进程的内存中为结果集分配相应的资源。这些资源由 MYSQL_RES 对象进行描述和管理。

因此,当我们遍历完结果集之后,就需要调用 mysql_free_result 释放结果集相关资源:

cpp 复制代码
mysql_free_result(res);

这里的 res 就是前面通过 mysql_store_result 得到的 MYSQL_RES* 结果集对象。调用 mysql_free_result 之后,该结果集对象以及其内部保存的结果数据都会被释放,后续就不能再继续访问 res,否则就会产生非法访问问题。

除了释放结果集之外,还需要关闭与 MySQL 服务端之间的连接。

前面我们知道,MYSQL 对象描述的是客户端与 MySQL 服务端之间的一次连接上下文。只要连接还没有关闭,客户端和服务端之间就仍然维护着相关连接状态。因此,当所有 SQL 操作都执行完毕之后,需要调用 mysql_close 关闭连接,并清理 MYSQL 对象内部维护的相关资源:

cpp 复制代码
mysql_close(mysql);

这里的 mysql 就是前面通过 mysql_init 初始化,并通过 mysql_real_connect 成功连接服务端的 MYSQL* 连接句柄。

需要注意的是,mysql_close 的清理行为和 MYSQL 对象本身的创建方式有关。

如果前面是这样初始化的:

cpp 复制代码
MYSQL* mysql = mysql_init(NULL);

那么 MYSQL 对象本身的空间是由 MySQL 客户端库内部申请的。此时调用:

cpp 复制代码
mysql_close(mysql);

不仅会关闭连接、清理内部资源,也会释放这个由客户端库申请出来的 MYSQL 对象本身。

而如果前面是这样写的:

cpp 复制代码
MYSQL mysql;
mysql_init(&mysql);

那么 MYSQL 对象本身的空间是用户在栈上提供的。此时调用:

cpp 复制代码
mysql_close(&mysql);

只会清理该对象内部维护的连接资源、网络状态、缓冲区等内容,不会释放 mysql 这个栈对象本身。因为栈对象的空间由当前函数的栈帧管理,函数退出时会自动回收。

所以,整个收尾流程可以概括为:

text 复制代码
如果获取了 SELECT 结果集:
    先调用 mysql_free_result(res) 释放 MYSQL_RES 结果集资源

如果已经建立了 MySQL 连接:
    再调用 mysql_close(mysql) 关闭连接并清理 MYSQL 连接句柄

也就是说:

text 复制代码
MYSQL_RES  用 mysql_free_result 释放
MYSQL      用 mysql_close 关闭和清理

这样整个 C/C++ 程序与 MySQL 服务端的交互流程才算完整结束。

C++ 调用 MySQL 客户端库完成数据库操作 Demo

结合前面所讲的 MySQL 客户端库调用流程,下面我实现了一个简单的 C++ Demo,用来演示如何通过 MySQL 客户端库连接数据库,并完成建表、插入数据、查询数据以及资源释放等基本操作:

cpp 复制代码
#include<iostream>
#include<mysql/mysql.h>
#include<string>

/*
 * 主函数:演示MySQL数据库的连接、表创建、数据插入和查询操作
 * 使用MySQL C API进行数据库操作
 */
int main()
{
    // ==========================================
    // 1. 初始化阶段
    // ==========================================
    
    // 声明MySQL连接对象指针,用于后续所有的数据库操作
    MYSQL* conn;
    
    // 初始化MySQL连接对象
    // mysql_init() 会分配或初始化一个适合与mysql_real_connect()函数使用的MYSQL对象
    // 参数传入NULL表示分配并初始化一个新的对象
    conn = mysql_init(NULL);

    // 检查初始化是否成功
    // 如果初始化失败(例如内存不足),mysql_init会返回NULL
    if (conn == NULL)
    {
        std::cerr << "mysql_init failed" << std::endl;  // 向标准错误流输出错误信息
        return 1;  // 返回非零值表示程序异常终止
    }

    // ==========================================
    // 2. 数据库连接阶段
    // ==========================================

    // 尝试连接到MySQL数据库服务器
    // 参数说明:
    // conn: 已初始化的连接对象指针
    // "localhost": 数据库服务器地址(本地)
    // "wz": 数据库用户名
    // "Wz123456!": 登录密码
    // "wz_db": 默认使用的数据库名称
    // 0: 端口号,0表示使用MySQL默认端口3306
    // NULL: Unix套接字或Windows命名管道,NULL表示使用默认机制
    // 0: 客户端标志,通常为0
    MYSQL* ret = mysql_real_connect(conn, "localhost", "wz", "Wz123456!", "wz_db", 0, NULL, 0);

    // 检查连接是否成功
    if (ret == NULL)
    {
        std::cerr << "mysql_real_connect failed" << std::endl;  // 输出连接失败提示
        std::cerr << mysql_error(conn) << std::endl;            // 输出MySQL服务器返回的具体错误详情
        mysql_close(conn);  // 连接失败也需要关闭连接对象,释放资源
        return 1;
    }

    // 输出连接成功信息
    std::cout << "Connected to MySQL database successfully!" << std::endl;

    // ==========================================
    // 3. 创建表 (DDL操作)
    // ==========================================

    // 定义创建表的SQL语句
    // 逻辑:如果表不存在则创建
    // 字段:
    // id: 整型,主键,自增
    // name: 变长字符串(50),非空
    // age: 整型,非空
    // telephone: 变长字符串(20),值必须唯一
    std::string query = "create table if not exists students(id int primary key auto_increment,name varchar(50) not null,age int not null,telephone varchar(20) unique)";

    // 执行SQL语句
    // mysql_query() 执行以空字符结尾的SQL查询字符串
    // 返回值为0表示成功,非0表示出错
    if (mysql_query(conn, query.c_str()) == 0)
    {
        std::cout << "Table created successfully!" << std::endl;  // 表创建成功
    }
    else
    {
        std::cerr << mysql_error(conn) << std::endl;  // 输出具体的SQL错误信息
        mysql_close(conn);  // 发生错误,关闭连接
        return 1;
    }

    // ==========================================
    // 4. 插入数据 (DML操作)
    // ==========================================

    // --- 插入第一条数据 ---
    // 准备插入数据的SQL语句:插入姓名、年龄、电话
    // 注意:id是自增的,所以不需要在这里指定
    query = "insert into students(name,age,telephone) values('Alice',20,'1234567890')";
    
    if (mysql_query(conn, query.c_str()) == 0)
    {
        std::cout << "Data inserted successfully!" << std::endl;
        
        // mysql_affected_rows(): 返回上次UPDATE、DELETE或INSERT操作更改/删除/插入的行数
        std::cout << "Affected rows: " << mysql_affected_rows(conn) << std::endl;
        
        // mysql_insert_id(): 返回由上一个INSERT或UPDATE语句为AUTO_INCREMENT列生成的值
        std::cout << "Last insert ID: " << mysql_insert_id(conn) << std::endl;
    }
    else
    {
        std::cerr << mysql_error(conn) << std::endl;
        mysql_close(conn);
        return 1;
    }

    // --- 插入第二条数据 ---
    query = "insert into students(name,age,telephone) values('Bob',22,'1234567891')";
    if (mysql_query(conn, query.c_str()) == 0)
    {
        std::cout << "Data inserted successfully!" << std::endl;
        std::cout << "Affected rows: " << mysql_affected_rows(conn) << std::endl;
        std::cout << "Last insert ID: " << mysql_insert_id(conn) << std::endl;
    }
    else
    {
        std::cerr << mysql_error(conn) << std::endl;
        mysql_close(conn);
        return 1;
    }

    // ==========================================
    // 5. 查询数据 (DQL操作)
    // ==========================================

    // 准备查询语句:从students表中查询id, name, age字段
    query = "select id,name,age from students";
    
    if (mysql_query(conn, query.c_str()) == 0)
    {
        // 检索完整的结果集到客户端
        // mysql_store_result() 将查询的所有数据读取到内存中
        MYSQL_RES* result = mysql_store_result(conn);

        // 检查结果集是否获取成功(例如对于SELECT语句通常应该有结果集)
        if (result == NULL)
        {
            std::cerr << mysql_error(conn) << std::endl;
            mysql_close(conn);
            return 1;
        }

        // 获取结果集中的字段数量(列数)
        int num_fields = mysql_num_fields(result);
        
        // 声明行数据变量
        MYSQL_ROW row;

        // 循环遍历结果集的每一行
        // mysql_fetch_row() 从结果集中获取下一行,当没有更多数据时返回NULL
        while ((row = mysql_fetch_row(result)))
        {
            // 遍历当前行的每一列
            for (int i = 0; i < num_fields; i++)
            {
                // row[i] 是一个字符串指针,如果该字段值为NULL,则row[i]为NULL
                if (row[i])
                {
                    std::cout << row[i] << " ";  // 输出字段值
                }
                else
                {
                    std::cout << "NULL ";        // 处理数据库中的NULL值
                }
            }
            std::cout << std::endl;  // 每行结束后换行
        }

        // 释放结果集占用的内存
        // 这是一个重要步骤,避免内存泄漏
        mysql_free_result(result);
    }
    else
    {
        std::cerr << mysql_error(conn) << std::endl;
        mysql_close(conn);
        return 1;
    }

    // ==========================================
    // 6. 清理与退出
    // ==========================================

    // 关闭服务器连接并释放连接对象占用的内存
    mysql_close(conn);
    
    return 0;  // 返回0表示程序正常执行完毕
}

结语

那么这就是本篇文章的全部内容,至此,MySQL系列就暂时告一段落,感谢各位读者的耐心阅读,而我之后会更新其他内容,希望你能够多多关注,如果本文有帮助到你的话,还请三连加关注,你的支持就是我创作的最大动力!感谢各位大佬对我的支持!

相关推荐
isyangli_blog1 小时前
7. 使用Mininet 创建回环网络拓扑
服务器·网络·php
xuhaoyu_cpp_java1 小时前
单调栈(算法)
java·数据结构·经验分享·笔记·学习·算法
千百元1 小时前
华为应用生成 .p12、.cer、.p7b
运维·服务器
小趴菜要进步2 小时前
Kali/Linux 更改国内镜像源
linux·运维·服务器
CSCN新手听安2 小时前
【Qt】Qt窗口(七)QColorDialog颜色对话框,QFileDialog文件对话框的使用
开发语言·c++·qt
A charmer2 小时前
从 C++ 到 Objective-C:零基础平滑转学专栏【总目录】
开发语言·c++·objective-c
代码中介商2 小时前
C/C++ 图形化界面编程入门:EasyX 完全指南
c语言
cookies_s_s2 小时前
C++ 内存模型与无锁编程:从底层原理到实战
linux·服务器·开发语言·c++
诙_2 小时前
C++数据结构--排序算法
数据结构·算法·排序算法