8.3 使用C语言访问MySQL数据

至此我们已掌握了MySQL的入门知识,下面让我们探究一下如何通过应用程序来访问MySQL,而不是使用GUI工具或基本的mysql客户端。

我们可以通过许多不同的编程语言来访问MySQL,包括:

❑ C

❑ C++

❑ Java

❑ Perl

❑ Python

❑ Eiffel

❑ Tcl

❑ Ruby

❑ PHP

Windows本地程序(如Access)也可以通过ODBC驱动程序来访问MySQL,甚至还有针对Linux的ODBC驱动程序,尽管我们没有什么理由来使用它。

在本章中,我们将主要讨论C语言接口,因为这是本书的重点,而且许多其他语言也使用相同的库来建立连接。

8.3.1 连接例程

用C语言连接MySQL数据库包含两个步骤:

❑ 初始化一个连接句柄结构;

❑ 实际进行连接。

首先,使用mysql_init来初始化连接句柄:

8.3 使用C语言访问MySQL数据 - 图1

通常你传递NULL给这个例程,它会返回一个指向新分配的连接句柄结构的指针。如果你传递一个已有的结构,它将被重新初始化。这个例程在出错时返回NULL。

目前为止,你只是分配和初始化了一个结构。你仍然需要使用mysql_real_connect来向一个连接提供参数:

8.3 使用C语言访问MySQL数据 - 图2

8.3 使用C语言访问MySQL数据 - 图3

指针connection必须指向已经被mysql_init初始化过的结构。其他参数的含义都相当明了,但是,请注意server_host既可以是主机名,也可以是IP地址。如果只是连接到本地机器,你可以通过指定localhost来优化连接类型。

sql_user_name和sql_password的含义和它们的字面含义一样。如果登录名为NULL,则假设登录名为当前Linux用户的登录ID。如果密码为NULL,你将只能访问服务器上无需密码就可访问的数据。密码会在通过网络传输前进行加密。

port_number和unix_socket_name应该分别为0和NULL,除非你改变了MySQL安装的默认设置。它们将默认使用合适的值。

最后,flags参数用来对一些定义的位模式进行OR操作,使得改变使用协议的某些特性。对于像本章这样的介绍性章节来说,这些标志都没什么用处,详细的资料请参考使用手册。

如果无法连接,它将返回NULL。mysql_error函数可以提供有帮助的信息。

使用完连接之后,通常在程序退出时,你要像下面这样调用函数mysql_close:

8.3 使用C语言访问MySQL数据 - 图4

这将关闭连接。如果连接是由mysql_init建立的,MySQL结构会被释放。指针将会失效并无法再次使用。保留一个不需要的连接是对资源的浪费,但是重新打开连接也会带来额外的开销,所以你必须自己权衡何时使用这些选项。

mysql_options例程(仅能在mysql_init和mysql_real_connect之间调用)可以设置一些选项:

8.3 使用C语言访问MySQL数据 - 图5

因为mysql_options一次只能设置一个选项,所以每设置一个选项就得调用它一次。你可以根据需要多次使用它,只要它出现在mysql_init和mysql_real_connect之间即可。并不是所有的选项都是char类型,因此它们必须被转换为const char *。表8-11中列出了3个最常用的选项。与往常一样,完整的选项请参见在线手册。

表 8-11

8.3 使用C语言访问MySQL数据 - 图6

一次成功的调用将返回0。因为它仅仅是用来设置标志,所以失败总是意味着使用了一个无效的选项。

如果要设置连接超时时间为7秒,我们使用的代码片断如下所示:

8.3 使用C语言访问MySQL数据 - 图7

8.3 使用C语言访问MySQL数据 - 图8

至此你已学会了如何建立和关闭连接,下面我们使用一个简短的程序来测试一下。

首先为用户设置一个新的密码(在下面的代码中,是本机上的rick用户),然后创建要连接的数据库foo。上述工作对你来说都应该很熟悉,所以我们将只显示它们执行的顺序:

8.3 使用C语言访问MySQL数据 - 图9

现在你已创建了新数据库。如果直接在mysql命令行中输入许多创建表和添加数据的命令,这比较容易出错,而且如果需要再次输入这些命令的话,这种方法也显得不够高效。为此,你应该创建一个包含你所需要命令的文件。

这个文件为create_children.sql:

8.3 使用C语言访问MySQL数据 - 图10

现在,你可以重新登录MySQL,选择数据库foo,并执行这个文件。为简洁起见,也为了避免将密码放入脚本中,我们将密码放在了命令行上:

8.3 使用C语言访问MySQL数据 - 图11

我们已删除了输出中的许多重复行,它们都是在数据库中创建行时生成的。现在你有一个用户、一个数据库和一个保存了一些数据的表,是时候看一下如何通过代码来访问这些数据了。

下面是源文件connect1.c,它以用户名rick和密码secret来连接本机服务器上名为foo的数据库:

8.3 使用C语言访问MySQL数据 - 图12

现在开始编译这个程序。你可能需要同时添加include路径和库文件路径,以及指定链接的库模块mysqlclient。在某些系统上,你可能还需要使用-lz选项来链接压缩库。在我的系统上,需要的编译指令为:

8.3 使用C语言访问MySQL数据 - 图13

你可能需要检查是否安装了客户端软件包,它们的安装位置取决于你所使用的Linux发行版,你需要根据它们的位置对上面的编译行做出相应的调整。

运行它时,你只会看到一条连接成功的信息:

8.3 使用C语言访问MySQL数据 - 图14

在第9章中,我们将演示如何通过创建一个makefile文件来将连接程序的构建自动化。

可以看出,与MySQL数据库建立连接是很简单的。

8.3.2 错误处理

在我们介绍更复杂的程序之前,了解一下MySQL如何进行错误处理是很有用的。MySQL使用一系列由连接句柄结构报告的返回码。两个必备的例程是:

8.3 使用C语言访问MySQL数据 - 图15

8.3 使用C语言访问MySQL数据 - 图16

你可以通过调用mysql_errno并传递连接结构来获得错误码,它通常都是非0值。如果未设定错误码,它将返回0。因为每次调用库都会更新错误码,所以你只能得到最后一个执行命令的错误码。但是上面列出的两个错误检查例程是例外,它们不会导致错误码的更新。

mysql_errno的返回值实际上就是错误码,它们在头文件errmsg.h或mysqld_error.h中定义。这两个文件都可以在MySQL的include目录中找到。前者报告客户端错误,后者关注服务端错误。

如果你更喜欢文本错误信息,也可以调用mysql_error,它提供了有意义的文本信息而不是单调的错误码。这些信息被写入一些内部静态内存空间中,所以如果想保存错误文本,你需要把它复制到别的地方。

你可以在代码中添加一些基本的错误处理来观察它们的行为。你可能已经注意到,当调用mysql_real_connect时会遇到一个问题,因为它在失败时返回NULL指针,并没有提供一个错误码。但如果你将连接句柄作为一个变量,那么即使mysql_real_connect失败,你仍然能够处理它。

下面是源文件connect2.c,它示例了如何使用非动态分配的连接结构,以及如何编写一些基本的错误处理代码。源文件中修改的部分以阴影显示:

8.3 使用C语言访问MySQL数据 - 图17

通过避免使用返回值覆盖连接指针的方法,你可以很容易地解决mysql_real_connect失败所带来的问题。不仅如此,这也是另一种使用连接结构的好例子。你可以使用一个错误的用户或密码来强制生成错误,从而得到类似于mysql工具提供的错误码。

8.3 使用C语言访问MySQL数据 - 图18

8.3.3 执行SQL语句

你已能够连接数据库并正确处理错误了,现在是时候让程序做一些实际的工作了。执行SQL语句的主要API函数被恰当的命名为:

8.3 使用C语言访问MySQL数据 - 图19

不是太难吧?这个例程接受连接结构指针和文本字符串形式的有效SQL语句(没有结束的分号,这与mysql工具不同)。如果成功,它返回0。对于包含二进制数据的查询,你可以使用第二个例程mysql_real_query,但是在本章中,我们将只使用mysql_query。

1.不返回数据的SQL语句

为简单起见,我们首先来看一些不返回任何数据的SQL语句:UPDATE、DELETE和INSERT。

我们将在这里介绍另一个重要函数,它用于检查受查询影响的行数:

8.3 使用C语言访问MySQL数据 - 图20

你很可能首先注意到的是这个函数的返回值类型很不常见。它使用无符号类型是出于移植性的考虑。当你使用printf时,我们推荐使用%lu格式将其转换为无符号长整型。这个函数返回受之前执行的UPDATE、INSERT或DELETE查询影响的行数。如果你使用过其他SQL数据库,MySQL的返回值可能会让你感到意外。MySQL返回的是被一个更新操作修改的行数,但许多其他数据库将仅仅因为记录匹配WHERE子句就把它视为已经更新过。

通常对于mysql_系列函数,返回值0表示没有行受到影响,正数则是实际的结果,一般表示受语句影响的行数。

首先,你需要在数据库foo中创建children表(如果你之前没有这么做的话)。删除(使用drop命令)任何已有的表以确保你有一个整洁的表定义,并重新发送在AUTO_INCREMENT列中使用的任何ID:

8.3 使用C语言访问MySQL数据 - 图21

现在,在connect2.c源文件中添加一些代码以在表中插入一个新行,这个新程序被命名为insert1.c。需要注意的是,下面代码中显示的折行是由于物理页面的限制,你通常不会在实际的SQL语句中使用换行符,除非它是一个非常长的语句。如果是这种情况,你可以在行尾使用\字符以允许SQL语句继续到下一行。

8.3 使用C语言访问MySQL数据 - 图22

毫不奇怪,我们插入了一行数据。

现在,让我们改变代码来包含UPDATE而不是INSERT,并且观察受影响的行是如何被报告的。

8.3 使用C语言访问MySQL数据 - 图23

我们将此程序叫做update1.c。它试图将所有叫做Ann的孩子的年龄设为4。

现在,假设children表中有如下数据:

8.3 使用C语言访问MySQL数据 - 图24

请注意有4个孩子的名字匹配Ann。如果执行update1,你可能会认为受影响的行数为4,这是由WHERE子句匹配的行数。但是,你会看到程序报告仅有2行受影响,这是因为实际需要对数据进行修改的行数只有2行。你可以使用mysql_real_connect的CLIENT_FOUND_ROWS标志来获得更传统的报告。

8.3 使用C语言访问MySQL数据 - 图25

如果你重置数据库中的数据,然后再运行程序,它将报告受影响的行数为4。

函数mysql_affected_rows还有最后一个古怪之处,它出现在从数据库中删除数据的时候。如果你使用WHERE子句删除数据,那么mysql_affected_rows将返回你期望的删除的行数。但如果在DELETE语句中没有WHERE子句,那么表中的所有行都会被删除,但是由程序返回的受影响行数却为0。这是因为MySQL优化了删除所有行的操作,它并不是执行许多个单行删除操作。这一行为不会受CLIENT_FOUND_ROWS选项标志的影响。

2.发现插入的内容

插入数据有一个微小但至关重要的方面。还记得我们提过AUTO_INCREMENT类型的列吗?它由MySQL自动分配ID。这一特性非常有用,特别是当你有许多用户的时候。

让我们再次查看表的定义:

8.3 使用C语言访问MySQL数据 - 图26

正如你看到的那样,childno列被设为AUTO_INCREMENT类型。这样当然很好,但是一旦你插入一行,你如何知道刚插入的孩子被分配了什么数字呢?

你可以执行一条SELECT语句来搜索孩子的名字,但这样效率会很低,并且如果有两个相同名字的孩子,这将不能保证唯一性。或者,如果同时有多个用户快速地插入数据,那么可能在更新操作和SELECT语句之间会有其他行被插入。因为发现一个AUTO_INCREMENT列的值是大家都面临的一个共同问题,所以MySQL以函数LAST_INSERT_ID( )的形式提供了一个专门的解决方案。

无论何时MySQL向AUTO_INCREMENT列中插入数据,MySQL都会基于每个用户对最后分配的值进行跟踪。用户程序可以通过SELECT专用函数LAST_INSERT_ID( )来发现该值,这个函数的作用有点像是表中的虚拟列。

实 验 提取由AUTO_INCREMENT生成的ID

你可以通过插入数据到表中并执行LAST_INSERT_ID( )函数来查看其作用。

8.3 使用C语言访问MySQL数据 - 图27

实验解析

每次插入一行,MySQL就分配一个新的id值并且跟踪它,使得你可以用LAST_INSERT_ID( )来提取它。

如果想通过实验查看返回的数字在本次会话中确实是唯一的,那么你可以打开另一个会话并插入另一行数据。然后在最初的会话中重新执行SELECT LAST_INSERT_ID( );语句。你将看到数字并没有发生改变,这是因为该语句返回的数字是由当前会话插入的最后一个数字。但是,如果执行SELECT *FROM children,你将看到其他会话确实已插入数据了。

实 验 在C程序中使用自动分配的ID

在本例中,我们将修改insert1.c程序以查看这些操作是如何在C语言中实现的。代码中的关键修改将以阴影显示。我们把修改后的程序命名为insert2.c。

8.3 使用C语言访问MySQL数据 - 图28

8.3 使用C语言访问MySQL数据 - 图29

下面是这个程序的输出:

8.3 使用C语言访问MySQL数据 - 图30

实验解析

在插入一行之后,你用LAST_INSERT_ID( )函数来获取分配的ID,就像常规的SELECT语句一样。然后使用mysql_use_result ( )从执行的SELECT语句中获取数据并将它打印出来,我们稍后将解释此函数。不要对刚才获取数值的机制过于担心,我们将在后面几页中介绍它们。

3.返回数据的语句

SQL最常见的用法当然是提取数据而不是插入或更新数据。数据是使用SELECT语句提取的。

MySQL也支持使用SQL语句SHOW、DESCRIBE和EXPLAIN来返回结果,但我们不会在这里涉及它们。按照惯例,手册中包含了对这些语句的解释。

在C应用程序中提取数据一般需要下面4个步骤:

❑ 执行查询;

❑ 提取数据;

❑ 处理数据;

❑ 必要的清理工作。

就像之前的INSERT和DELETE语句一样,你将使用mysql_query来发送SQL语句。然后,你使用mysql_store_result或mysql_use_result来提取数据,具体使用哪个函数取决于你想如何提取数据。接着,你将使用一系列mysql_fetch_row调用来处理数据。最后,使用mysql_free_result释放查询占用的内存资源。

mysql_use_result和mysql_store_result的区别主要在于,你是想一次返回一行数据,还是一次返回所有的结果。当你预计结果集比较小时,后者会更加合适。

● 一次提取所有数据的函数

你可以使用mysql_store_result在一次调用中从SELECT(或其他返回数据的语句)中提取所有数据:

8.3 使用C语言访问MySQL数据 - 图31

显然,你需要在成功调用mysql_query之后使用此函数。这个函数将立刻保存在客户端中返回的所有数据。它返回一个指向结果集结构的指针,如果失败则返回NULL。

在mysql_store_result调用成功之后,你需要调用mysql_num_rows来得到返回记录的数目,我们希望这是个正数,但是如果没有返回行,这个值将是0。

8.3 使用C语言访问MySQL数据 - 图32

这个函数接受由mysql_store_result返回的结果结构,并返回结果集中的行数。如果mysql_store_result调用成功,mysql_num_rows将始终都是成功的。

通过对这些函数的组合使用,你获得了一种提取你所需要数据的简单方法。到了这里,所有数据对于客户端来说都是本地的,你不再需要担心可能的网络或数据库错误了。对返回行数的获取将有助于你进行随后的编程。

如果你碰巧使用的是一个特别庞大的数据集,那么最好提取小一些、更容易管理的信息块,因为这将更快地将控制权返回给应用程序,并且不会占用大量的网络资源。我们将在介绍mysql_use_result的时候,详细探讨这一想法。

现在,你可以使用mysql_fetch_row来处理它,也可以使用mysql_data_seek、mysql_row_seek和mysql_row_tell在数据集中来回移动。下面让我们来看看这些函数。

❑ mysql_fetch_row:这个函数从使用mysql_store_result得到的结果结构中提取一行,并把它放到一个行结构中。当数据用完或发生错误时返回NULL。我们将在下一节中回过来处理行结构中的数据。

8.3 使用C语言访问MySQL数据 - 图33

❑ mysql_data_seek:这个函数用来在结果集中进行跳转,设置将会被下一个mysql_fetch_row操作返回的行。参数offset的值是一个行号,它必须在0到结果集总行数减1的范围内。传递0将会导致下一个mysql_fetch_row调用返回结果集中的第一行。

8.3 使用C语言访问MySQL数据 - 图34

❑ mysql_row_tell:这个函数返回一个偏移值,它用来表示结果集中的当前位置。它不是行号,你不能把它用于mysql_data_seek。

8.3 使用C语言访问MySQL数据 - 图35

❑ 但是,你可以这样使用它的返回值:

8.3 使用C语言访问MySQL数据 - 图36

这将在结果集中移动当前位置,并返回之前的位置。

这对函数对于在结果集中的已知点之间的移动非常有用。但请小心不要混淆了由row_tell和row_seek使用的偏移量和data_seek使用的行号。否则,结果将变得不可预知。

❑ 完成了对数据的所有操作后,你必须明确地调用mysql_free_result来让MySQL库完成善后处理。

8.3 使用C语言访问MySQL数据 - 图37

❑ 完成了对结果集的操作后,你必须总是调用此函数来让MySQL库清理它分配的对象。

● 提取数据

现在可以编写你的第一个数据提取应用程序了。你想要选择所有年龄大于5的记录。因为还不知道如何处理这些数据,所以你将仅仅提取它们。提取结果集并遍历提取数据的重要代码片断用阴影显示。下面是select1.c的源代码:

8.3 使用C语言访问MySQL数据 - 图38

8.3 使用C语言访问MySQL数据 - 图39

● 一次提取一行数椐

为了逐行提取数据——如果这是你真正想要的,你将依靠mysql_use_result而不是mysql_store_result。

8.3 使用C语言访问MySQL数据 - 图40

与mysql_store_result函数一样,mysql_use_result在遇到错误时也返回NULL。如果成功,它返回指向结果集对象的指针。但是,不同之处在于它未将提取的数据放到它初始化的结果集中。

为了真正得到数据,你必须反复调用mysql_fetch_row直到提取了所有的数据。如果没有mysql_use_result中得到所有数据,那么程序中后续的提取数据操作可能会返回遭到破坏的信息。

那么,调用mysql_use_result和调用mysql_store_result的效果有何不同呢?前者具备资源管理方面的实质性好处,但是它不能与mysql_data_seek、或mysql_row_seek或mysql_row_tell一起使用,并且由于直到所有数据都被提取后才能实际生效,mysql_num_rows的使用也受到限制。

你还增加了时延,因为每个行请求和结果的返回都必须通过网络。另外还存在一种可能性是,网络连接可能在操作中途失败,留给你不完整的数据。

但是,无论怎样,这些都不会抹去我们之前提到的它带来的好处:更好地平衡了网络负载,以及减少了可能非常大的数据集带来的存储开销。

把select1.c修改为select2.c,这里将使用mysql_use_result函数。因为很简单,所以我们仅仅以阴影方式显示修改的代码片断:

8.3 使用C语言访问MySQL数据 - 图41

注意观察,在提取最后一个结果之前,你仍然无法得到行数。但是,通过早期和经常性的错误检查,可以使得程序调整为使用mysql_use_result变得更加容易。以这种方式编写代码可以减少许多程序后期修改带来的烦恼。

4.处理返回的数据

现在你已知道了如何提取行,下面可以学习如何处理返回的实际数据了。

如同大多数SQL数据库一样,MySQL返回两种类型的数据。

❑ 从表中提取的信息,也就是列数据。

❑ 关于数据的数据,即所谓的元数据(metadata),例如列名和类型。

让我们首先关注如何将数据本身转化为有用的形式。

mysql_field_count函数提供了一些关于查询结果的基本信息。它接受连接对象,并返回结果集中的字段(列)数目:

8.3 使用C语言访问MySQL数据 - 图42

在更通用的方式下,你可以用mysql_field_count做其他事情,比如判断为何mysql_store_result的调用会失败。例如,如果mysql_store_result返回NULL,但是mysql_field_count返回一个正数,你可以推测这是一个提取错误。但是,如果mysql_field_count返回0,则表示没有列可以提取,这可以解释为何存储结果会失败。我们有理由认为,你应该了解一个特定查询应返回的列数。因此,对于通用查询处理模块或任何随意构造查询的情况,这个函数是非常有用的。

在为旧版本的MySQL所写的代码中,你可能会看到使用mysql_num_fields的情况。它可以接受一个连接结构或一个结果结构指针作为参数,并返回列数。

如果抛开对数据的格式化不管,那么你已经知道如何立刻打印出数据了。你可以添加简单的display_row函数到select2.c程序中。

请注意,为了简化程序,你把连接、结果和mysql_fetch_row返回的行信息都设为全局的。我们并不建议在产品代码中这样做。

(1)下面是非常简单的打印数据的代码:

8.3 使用C语言访问MySQL数据 - 图43

8.3 使用C语言访问MySQL数据 - 图44

(2)将它添加到select2.c中,并添加一个声明和一个函数调用:

8.3 使用C语言访问MySQL数据 - 图45

(3)现在,把完成的代码保存为select3.c。最后,按如下方式编译并运行select3:

8.3 使用C语言访问MySQL数据 - 图46

看来,程序可以运行了,虽然它的输出不是特别美观。但是你并未考虑结果中可能出现的NULL值。如果想要打印出更整洁的格式化(或许是表格化)的数据,你需要同时得到MySQL返回的数据和元数据。你可以使用mysql_fetch_field来同时将元数据和数据提取到一个新的结构中:

8.3 使用C语言访问MySQL数据 - 图47

你需要重复调用此函数,直到返回表示数据结束的NULL值为止。然后,你可以使用指向字段结构数据的指针来得到关于列的信息。结构MySQL_FIELD定义在mysql.h中,如表8-12所示。

表 8-12

8.3 使用C语言访问MySQL数据 - 图48

列类型相当广泛。完整列表见头文件mysql_com.h和文档。常见的有:

8.3 使用C语言访问MySQL数据 - 图49

一个特别有用的预定义宏为IS_NUM,当字段类型为数字时,它返回true,像下面这样:

8.3 使用C语言访问MySQL数据 - 图50

在更新程序之前,我们还需要提及一个函数:

8.3 使用C语言访问MySQL数据 - 图51

你可以用此函数来覆盖当前的字段编号,该编号会随每次mysql_fetch_field调用而自动增加。如果给参数offset传递值0,你将跳回第一列。

现在你得到信息了,你需要让select程序显示和某一指定列相关的所有额外数据。

下面是程序select4.c,我们在这里重新完整地显示了整个程序的源代码,这样你就可以看到一个完整的例子了。注意,它并没有试图对列类型进行详尽的分析。

8.3 使用C语言访问MySQL数据 - 图52

8.3 使用C语言访问MySQL数据 - 图53

8.3 使用C语言访问MySQL数据 - 图54

编译并运行此程序时,你得到的输出为:

8.3 使用C语言访问MySQL数据 - 图55

这仍然不是很漂亮,但它很好地阐明了如何通过同时处理原始数据和元数据来更有效地使用数据。

你还可以通过其他一些函数来提取字段数组并在列间进行跳转。但通常你需要使用的所有例程都在这里介绍了,感兴趣的读者也可以在MySQL手册中找到更多信息。

8.3.4 更多的函数

表8-13中显示了其他一些我们建议你了解的API函数。一般情况下,到目前为止介绍的所有函数对于实现一个可工作的程序已足够了,但是,你将会发现下面这个挑选过的列表也很有用。

表 8-13

8.3 使用C语言访问MySQL数据 - 图56