MySQL 专家级教程(三)
六、嵌入式 MySQL
MySQL 服务器以其轻量级和高性能特性而闻名,但是您知道它也可以用作企业应用的嵌入式数据库吗?本章解释了嵌入式应用的概念,以及如何使用 MySQL C API 来创建自己的嵌入式 MySQL 应用。我将向您介绍编译嵌入式服务器和为 Linux 和 Windows 编写应用的技术。
构建嵌入式应用
已经使用轻量级数据库系统作为内部数据存储构建了许多应用。如果您使用 Microsoft Windows 作为主要的桌面操作系统,那么您很可能已经见过或使用过至少一个使用 Microsoft Access 数据库引擎的应用。即使应用没有宣传 Access 的使用,通常只要粗略地看一下安装目录就可以知道。
一些嵌入式应用使用主机上现有的数据库系统(如 Access),而另一些则使用大型数据库系统的专用安装。不太明显的是那些将数据库系统编译进软件本身的应用。
什么是嵌入式 系统?
一个嵌入式系统是包含在另一个系统 中的系统。简单来说,嵌入式系统是主机系统的从机。嵌入式系统的目的是提供主机系统需要的一些功能。这可能是通信机制、数据存储和检索,甚至是图形用户显示。
嵌入式系统传统上被认为是专用的硬件或电子设备。例如,自动柜员机(ATM)是包含专用硬件的嵌入式系统。今天,嵌入式系统不仅包括专用硬件,还包括专用软件系统。与难以或不可能修改的嵌入式硬件不同,嵌入式软件经常被修改以在特定环境中工作。嵌入式硬件和软件都具有独立的特性,并为主机系统提供一些服务。
嵌入式软件系统通常与您日常看到和使用的应用不同。有些,比如那些使用嵌入式 MySQL 库的,是对现有功能的改编,是为了在另一个软件系统中更有效地工作。然而,与其独立的服务器版本不同,嵌入式 MySQL 服务器被设计为在编程级别上运行。也就是说,对服务器的调用是通过编程语言完成的,而不是作为特别的查询。方法在嵌入式服务器中公开,以将特定查询作为参数,并启动服务器来执行它们。
这意味着嵌入式 MySQL 服务器只能通过另一个应用来访问。正如您将在接下来的几节中看到的,嵌入式软件可以存在于许多应用中,其集成级别从封闭的仅编程访问到被宿主应用“隐藏”的全功能系统不等。让我们先来看看最常见的嵌入式系统类型。
嵌入式系统的类型
由于嵌入式系统使用的独特性,很难对其进行分类。它们通常属于以下一个(或多个)类别:
实时:在要求主机系统在给定阈值内做出响应和动作的设施中使用的系统。这套系统最常见的特征是计时。每个命令过程的执行时间必须最小化以实现系统的目标。通常,这些系统需要在外部发生的事件内执行,而不是以任何内部处理速度执行。路由或电信交换机就是实时系统的一个例子。
反应型:只对外部事件做出反应的系统。这些事件在本质上倾向于重复发生和循环,但它们也可能以用户输入的形式出现(交互系统是反应式系统)。反应系统被设计成总是可操作的。定时通常是次要的,只受循环操作频率的限制。反应式系统的一个例子是安全监控系统,该系统被设计成当某些事件或阈值发生时寻呼或警告服务人员。
过程控制:用于控制其他系统的系统。这些往往是那些旨在监测和控制硬件设备,如机器人和加工机械。这些系统通常被编程为重复一系列动作,并且通常不会偏离其预期的编程或响应外部事件或状态变量或条件的阈值。过程控制系统的一个例子是在装配汽车特定部件的汽车装配线上使用的机器人。
关键:用于具有高成本因素的设施中的系统,例如安全、医疗或航空。这些系统被设计成不会失败(或者永远不会失败)。这些系统通常包括前面描述的嵌入式系统的变体。关键系统的例子包括医疗系统,如呼吸器或人工循环系统。
嵌入式数据库系统
一个嵌入式数据库 系统是一个为主机应用或环境提供数据的系统。这些数据通常是在进程中被请求的,因此数据库必须响应请求并毫不延迟地返回任何信息。嵌入式数据库系统被认为对主机应用和整个系统至关重要。因此,嵌入式数据库系统也必须满足用户的时间要求。这些需求意味着嵌入式数据库系统通常被归类为反应式系统。
除了最普通的应用,个人和企业使用的所有应用都生产、消费和存储数据。许多应用的数据结构良好,对客户具有内在价值。事实上,在许多情况下,数据是自动持久化的,客户希望在需要的时候数据是可用的。此类应用的子系统要么具有访问方法,要么连接到外部文件或数据处理系统,如数据库服务器。
使用文件访问数据的嵌入式系统面临着许多问题,尤其是数据是否可以在主机应用之外访问。在这种情况下,访问限制可能必须从头开始创建,或者作为系统中的另一层添加。文件系统通常具有非常好的性能,并提供更快的访问时间,但它们不如数据库系统灵活。数据库系统在存储数据的形式上提供了更多的灵活性(作为表格而不是结构化文件),但通常会导致访问速度较慢。
虽然保护数据的原因可能多种多样,但最基本的要求是以最有效的方式存储和检索数据,而不将数据暴露给其他人。很多时候这仅仅是对数据库系统的需要。例如,Adobe Bridge 之类的应用管理着 Adobe 生产工具套件中使用的大量文件、项目、照片等数据。这些文件需要以一种易于搜索和检索的方式进行组织。Adobe 使用嵌入式数据库(MySQL)来管理 Adobe Bridge 存储的文件的元数据。在这种情况下,应用使用数据库系统来处理更困难的工作,即存储、搜索和检索关于它所管理的对象的元数据。
由于数据必须受到保护,使用外部数据库系统的选择变得有限,因为完全保护(或隐藏)数据并不总是容易或可能的。嵌入式数据库系统允许应用使用数据库系统的全部功能,同时对外部来源隐藏机制和数据。
嵌入 MySQL
MySQL 工程师在 MySQL 开发的早期就认识到,它的许多客户都是系统集成商,需要一个健壮、高效、可编程访问的数据库系统。他们不仅提供了一个嵌入式库,还提供了一个全功能的客户端库。客户端库允许您创建自己的 MySQL 客户端。例如,您可以创建自己版本的 MySQL 命令行客户端。客户端库被命名为libmysql。如果您想了解典型的 MySQL 客户端如何使用这个库,请查看mysql项目源文件。
MySQL 嵌入式库以服务器可执行文件的名称命名为libmysqld。您可能会看到这个库被称为嵌入式服务器,或者简称为 C API。本章专门介绍嵌入式库(libmysqld),但是,客户端和嵌入式服务器库之间的大部分访问和连接是相似的。
嵌入式库提供了许多通过应用编程接口(API)访问数据库系统的功能。API 提供了许多允许系统利用 MySQL 服务器的特性(通过编程)。这些特性包括:
- 连接并建立服务器实例
- 断开与服务器的连接
- 使用受控(安全)机制关闭服务器
- 操作服务器启动选项
- 处理错误
- 生成 DBUG 跟踪文件
- 发出查询并检索结果
- 管理数据
- 访问 MySQL 服务器的(几乎)全部特性集
最后一点是独立服务器和嵌入式服务器之间最重要的区别之一。嵌入式服务器不使用完整的身份验证机制,默认情况下是禁用的。这是嵌入式 MySQL 系统难以保护的原因之一(更多细节请参见后面的“安全性问题”)。但是,您可以使用配置选项--with-embedded-privilege-control打开认证,并重新编译嵌入式服务器。除此之外,在特性和功能方面,服务器的行为与独立服务器几乎相同。
真正酷的是,由于嵌入式库使用与独立服务器相同的访问方法,所以您使用独立服务器创建的所有数据库和表都可以与嵌入式服务器一起使用。这允许您创建表并使用独立的服务器测试它们,然后将它们移动到嵌入式系统中。尽管可以让两台服务器访问同一个数据目录,但这是绝对不鼓励的,因为这可能会导致数据丢失和不可预测的行为(永远不要在 MySQL 服务器实例之间“共享”数据目录)。
这是否意味着您可以将独立服务器作为嵌入式服务器在同一台机器上运行?不仅是可以,您想要多少台嵌入式服务器?只要嵌入式服务器实例没有使用同一个数据目录,就可以同时运行几个。每个人管理的数据与其他人管理的数据是分开的,没有数据共享。我在自己的系统上试了一下,效果不错。我有一个 5.6.9 嵌入式应用,与我的 5.1(正式上市)GA 独立服务器一起运行。在撰写本文时,MySQL 5.5 是最新的 GA 版本。我不必停止甚至中断单机来与嵌入式服务器交互。多酷啊。
嵌入 MySQL 的方法
有许多类型的嵌入式应用。嵌入式数据库应用通常分为三类。它们要么部分隐藏在另一个接口后面(捆绑嵌入),要么隐藏在一个包装或包含数据库服务器的系统后面(深度嵌入)。下面几节描述了与嵌入 MySQL 系统相关的每一种类型。
捆绑服务器嵌入
捆绑服务器嵌入是一个独立安装 MySQL 服务器构建的系统。服务器级嵌入式系统通过关闭外部(网络)访问来隐藏 MySQL 服务器,而不是让系统或网络上的任何人都可以使用 MySQL。因此,这种形式的嵌入式 MySQL 系统只是一个关闭了网络访问(TCP/IP)的独立服务器。
这种类型的嵌入式 MySQL 系统的优势在于,可以使用本地安装的(并正确配置的)客户端应用来维护服务器。因此,系统集成商、管理员和开发者可以使用常规的管理和开发工具来维护嵌入式 MySQL 服务器,而不必使用外部应用加载数据。
服务器级嵌入式 MySQL 系统的一个例子是 LeapTrack 软件,由 LeapFrog ( www.leapfrogschoolhouse.com/do/findsolution?detailPage=overview&name=ReadingPro)生产。MySQL 报告称,LeapFrog 选择 MySQL 是因为其跨平台支持,允许 LeapFrog 在各种平台上提供其产品,而不改变核心数据库功能。在那之前,LeapFrog 一直为其各种平台使用不同的专有数据库解决方案。
深度嵌入 (libmysqld)
深度嵌入甚至比捆绑嵌入更具限制性。这种类型的嵌入式系统使用 MySQL 系统作为一个完整的组件。这意味着 MySQL 系统不仅不能从网络访问,而且也不能从普通的客户端应用访问。相反,该系统是使用 Oracle 提供的名为libmysqld的特殊嵌入式库构建的。大多数嵌入式 MySQL 系统都属于这一类。
因为这种类型的嵌入式系统仍然使用 MySQL 机制进行数据访问,所以它提供了相同的数据库功能,只有一些限制(我将稍后讨论)。开发者能够通过各种开发语言在各种平台上使用深度嵌入的 MySQL 系统(正如我前面解释的)。此外,它为开发者提供了一个代码级的解决方案,这是很少关系数据库系统能够提供的。
使用深度嵌入的 MySQL 系统的最大优势是,它提供了一个几乎完全隔离的 MySQL 系统,单独服务于嵌入式应用的目的。
深度嵌入的 MySQL 应用的一个例子是 Adobe 公司的 Adobe Bridge(www.adobe.com/products/bridge.html)。Adobe Bridge 是更大的 Adobe Creative Suite 的一部分,用于管理 Creative Suite 支持的数据的各个方面,而最终用户并不知道他们正在运行一个专用的 MySQL 系统。 1 大多数深度嵌入式系统都是用户安装在本地计算机上的桌面应用。
资源需求
运行嵌入式服务器的要求取决于嵌入的类型。如果使用捆绑嵌入,要求与独立安装的要求相同。然而,一个深度嵌入的 MySQL 系统是不同的。除了应用的需求之外,深度嵌入式系统还需要大约 2MB 的内存来运行。编译后的嵌入式服务器为可执行内存增加了相当多的空间,但并不繁重或难以管理。
磁盘空间是要考虑的最不可预测的资源。这是真的,因为它确实取决于嵌入式系统使用了多少数据。对于高吞吐量系统或处理大量数据更改的系统,磁盘空间和时间也是需要考虑的问题。处理对数据的大量更改对响应时间的影响往往大于对所用空间的影响。在这些情况下,数据库的维护可能需要对服务器的特殊访问或特殊接口,以允许管理员访问数据。这是一个很好的例子,在这个例子中,以捆绑的嵌入形式访问数据库服务器比使用深度嵌入更容易。
安全问题
安全性是另一个依赖于嵌入类型的领域。如果系统是使用服务器嵌入构建的,解决安全问题可能会非常具有挑战性。这是真的,因为 MySQL 系统仍然可以从本地服务器使用正常的工具集进行访问。完全锁定这种类型的嵌入式系统可能非常困难。
捆绑嵌入要容易得多,因为嵌入式独立 MySQL 系统只能通过嵌入式应用访问。除非嵌入式应用开发者有一个失调的道德指南针,否则他们会采取措施来确保正确的凭证是访问管理功能所必需的。
深度嵌入式系统是保护数据最困难的情况。嵌入式 MySQL 系统可能没有为其设置任何密码(它们通常没有),因为与捆绑嵌入一样,它们要求用户使用提供的接口来访问数据。不幸的是,事情没那么简单。在许多情况下,数据放在用户可以访问的目录中。事实上,数据需要用户可以访问;否则,她怎么能读取数据呢?
这就是问题所在。数据文件不受保护,可以使用另一个 MySQL 安装进行复制和访问。这不仅限于嵌入式服务器;这也是单机服务器的一个问题。这令人震惊吗?如果你的组织在开源软件的使用上有严格控制的限制,这是可能的。想象一下当你的信息保证官发现时他脸上的表情。好吧,所以你最好委婉地告诉他。因此,可能需要在嵌入式应用中包含额外的安全特性,以适当地保护嵌入式 MySQL 系统及其数据。
MySQL 嵌入的优势
MySQL 嵌入式 API 使开发者能够在另一个应用中使用全功能的 MySQL 服务器。最重要的好处是提高了数据访问的速度(因为服务器要么是应用的一部分,要么与应用运行在同一硬件上),内置的数据库管理工具,以及非常灵活的存储和检索机制。这些好处使开发者有机会整合使用 MySQL 的所有好处,同时对用户隐藏其实现。这意味着开发者可以通过利用 MySQL 的特性来增加他们自己产品的功能。
MySQL 嵌入的局限性
使用嵌入式 MySQL 服务器有一些限制。幸运的是,这是一个简短的列表。大多数限制是合理的,通常对系统集成商来说不是问题。表 6-1 列出了使用嵌入式 MySQL 系统的已知限制。每个都包括一个简短的描述。
表 6-1。使用嵌入式 MySQL 的局限性
| 限制 | 描述 |
|---|---|
| 安全 | 默认情况下,访问控制是关闭的。特权系统处于非活动状态。 |
| 复制 | 没有复制或记录功能。 |
| 从外部接近 | 不允许外部网络通信(除非您自己构建)。 |
| 装置 | 深度嵌入的应用(如libmysqld)可能需要额外的库来部署。 |
| 事件 | 事件计划程序不可用。 |
| 数据 | 嵌入式服务器像独立服务器一样存储数据,为每个数据库使用一个文件夹,为每个表使用一组文件。 |
| 版本 | 嵌入式服务器不支持 MySQL 5.1 的某些版本。 |
| 南非民主统一战线(United Democratic Front) | 不允许使用用户定义的函数。 |
| 调试/跟踪 | 核心转储不会生成堆栈跟踪。 |
| 连通性 | 您不能通过网络协议连接到嵌入式服务器。请注意,您可以通过嵌入式应用提供这种外部访问。 |
| 资源 | 如果使用捆绑嵌入并支持大量数据和/或许多同时连接,可能会很繁重。 |
MySQL C API
乍一看 MySQL C API 文档(MySQL 参考手册中题为“API 和库”的一章)可能会令人生畏。嗯,确实是。C API 旨在封装独立服务器的所有功能。这不是一项简单或容易的任务。幸运的是,Oracle 在http://dev.mysql.com/doc提供了对 MySQL 文档的在线访问。查找在线参考手册的“libmysqld,嵌入式 MySQL 服务器库”小节。
注意在线文档通常是最新版本。如果为了方便起见,您已经下载了一个副本,您可能希望定期检查联机文档。通过重新检查在线文档,我找到了几个绊脚石的答案。
具有讽刺意味的是,也许 C API 最令人生畏的方面是文档本身。简单地说,它有点简洁,需要通读几遍,概念才会变得清晰。我的目标是以一个简短的教程和几个例子的形式向您介绍 C API,以帮助您快速启动嵌入式应用项目。
入门指南
我给想学习如何构建嵌入式应用的开发者的第一个建议是阅读文档。尽管有目前的文本和章节,在开始使用 API 之前通读产品文档总是一个好主意,即使你没有马上接受这些信息。我经常在 MySQL 文档中发现一些表面上看起来无关紧要的信息,但后来证明这些信息是成功编译和令人沮丧地寻找错误来源之间的关键。
我还建议登录 MySQL 网站,浏览论坛(在http://forums.mysql.com有一个专门的嵌入式论坛)和邮件列表(http://lists.mysql.com)存储库。您不必阅读所有内容,但是您的一些问题可能可以通过阅读这些存储库中的条目得到解答。我有时也会看看 MySQL 博客(www.planetmysql.org)。许多作者已经发布了关于嵌入式服务器和许多其他感兴趣的项目的信息。有如此多有趣的信息,有时我发现自己一次要读一个多小时。许多 MySQL 专家认为这种策略是成为 MySQL 大师的关键。信息就是力量。
在线文档、各种列表和博客绝对是 MySQL 最新信息的最佳来源。你应该做的最重要的阅读包含在以下章节中。我将介绍主要的 C API 函数,并通过一个简单的嵌入式应用示例进行演示。稍后,我将演示一个更复杂的嵌入式应用,用一个抽象的数据访问类完成,并用. NET 编写。
学习如何创建嵌入式应用的最好方法是自己编写一个。请随意打开您最喜欢的源代码编辑器,跟着我演示几个例子。我首先按照需要调用的顺序遍历每个需要调用的函数。然后,在后面的部分中,我将向您展示如何构建这个库并编写您的第一个嵌入式服务器应用。
最常用的功能
快速浏览一下文档就会发现 C API 支持超过 65 个函数。其中一些已经被否决了,但是 Oracle 非常擅长在文档中指出这一点(这是阅读它的另一个好理由)。只有少数功能是经常使用的。
库中的大多数函数都提供了连接和服务器操作函数。一些专用于收集关于服务器和数据的信息,而另一些用于提供执行查询和其他数据操作的调用。还有检索错误信息的函数。
表 6-2 列出了最常用的功能。包括函数的名称、简短描述和定义函数的源文件。这些函数大致按照在一个简单的嵌入式服务器示例中被调用的顺序列出。
表 6-2 。最常用的功能
| 功能 | 描述 | 来源 |
|---|---|---|
| mysql_server_init() | 初始化嵌入式服务器库。 | libmysql.c |
| mysql_init() | 启动服务器。 | 客户端. c |
| mysql_options() | 允许您更改或设置服务器选项。 | 客户端. c |
| mysql_debug() | 打开调试跟踪文件(DBUG)。 | libmysql.c |
| mysql_real_connect() | 建立与嵌入式服务器的连接。 | 客户端. c |
| mysql_query() | 发出查询语句(SQL)。语句作为空终止字符串传递。 | libmysql.c |
| mysql 存储结果() | 检索上次查询的结果。 | 客户端. c |
| mysql_fetch_row() | 从结果集中返回一行。 | 客户端. c |
| mysql_num_fields() | 返回结果集中的字段数。 | 客户端. c |
| mysql_num_rows() | 返回结果集中的行数(记录数)。 | 客户端. c |
| mysql_error() | 返回描述上一个错误的格式化错误消息(字符串)。 | 客户端. c |
| mysql_errno() | 返回上一个错误的错误号。 | 客户端. c |
| mysql 自由结果() | 释放分配给结果集的内存。注意:不要忘记经常使用这个功能。对空结果集调用此方法不会生成错误。 | 客户端. c |
| mysql_close() | 关闭与服务器的连接。 | 客户端. c |
| mysql 服务器端() | 完成嵌入式服务器库并关闭服务器。 | libmysql.c |
注我鼓励你在通读完本章并理解示例之后,花些时间通读 MySQL 参考手册 C API 部分的函数列表。您可能会发现一些满足您特殊数据库需求的有趣函数。
有关这些函数的完整描述,包括返回值和用法,请参见 MySQL 参考手册。
创建嵌入式服务器
在初始化函数调用期间,嵌入式服务器被建立为实例。大多数函数需要一个指向服务器实例的指针作为必需的参数。当您创建一个嵌入式 MySQL 应用时,您需要创建一个指向MYSQL对象的指针。您还需要为结果集和结果集中的一行(称为记录)创建实例。幸运的是,服务器的定义和主要结构都在 MySQL 头文件中定义。您需要使用的头文件(对于大多数应用)是:
#include <stdio.h>
#include <stdlib.h>
#include <stdarg.h>
#include <mysql.h>
使用以下语句可以创建指向嵌入式服务器、结果集和记录结构的指针变量:
MYSQL *mysql; // the embedded server class
MYSQL_RES *results; // stores results from queries
MYSQL_ROW record; // a single row in a result set
这些语句允许您访问嵌入式服务器(MYSQL)、结果结构(MYSQL_RES)和记录(MYSQL_ROW)。您可以使用全局变量来定义这些指针。你们中的一些人可能不喜欢使用全局变量,也没有理由一定要使用。结果集和记录可以随意创建和销毁。只要确保在整个应用中保持MYSQL指针变量是同一个实例。
我们还没完成设置。我们仍然需要建立一些字符串,以便在连接过程中使用。我见过许多不同的方法来实现这一点,但最流行的方法是创建一个字符串数组。至少,您需要为my.cnf(在 Windows 中为my.ini)文件的位置和数据的位置创建字符串。一组典型的初始化字符串是:
static char *server_options[] = {"mysql_test",
"--defaults-file=c:\\mysql_embedded\\my.ini",
"--datadir=c:\\mysql_embedded\\data", NULL };
本章中的示例描述了 Windows 编译的服务器选项。如果您使用 Linux,您将需要使用适当的路径并将my.ini更改为my.cnf。在这个例子中,我使用了标签"mysql_test"(被mysql_server_init()忽略),my.cnf (my.ini)文件的位置到普通安装目录,数据目录到普通 MySQL 安装。如果要建立独立服务器和嵌入式服务器,应该为每台服务器使用不同的数据位置。为了保持整洁,您可能还想使用不同的配置文件。
为了帮助将错误降到最低,我还使用了一个整数变量来标识字符串数组中元素的数量(我稍后将讨论这一点)。这允许我编写边界检查代码,而不必记住允许多少个元素。我可以允许元素的数量在运行时改变,从而允许边界检查代码在必要时适应变化。
int num_elements=(sizeof(server_options) / sizeof(char *)) - 1;
最后一个设置步骤是创建另一个字符串数组,它标识包含我的配置文件(my.cnf)中任何附加服务器选项的服务器组。这定义了服务器启动时将要读取的部分。
static char *server_groups[] = {"libmysqld_server", "libmysqld_client", NULL };
正在初始化服务器
在连接到嵌入式服务器之前,必须对其进行初始化或启动。这通常涉及两个初始化调用,然后是任意数量的设置附加选项的调用。启动嵌入式服务器需要调用的第一个初始化函数是mysql_server_init() 2 。该功能定义为:
int mysql_server_init(int argc, char **argv, char **groups)
在调用任何其他函数之前,该函数只被调用一次。它将参数argc和argv,作为程序的普通参数(与 main 函数相同)。此外,来自配置的组标签被传递以允许服务器读取运行时服务器选项。返回值要么是 0 表示成功,要么是 1 表示失败。这允许您在条件语句中调用函数,并在出现故障时采取行动。下面是使用启动部分的声明调用该函数的示例:
mysql_server_init(num_elements, server_options, server_groups);
注意为了让示例简短易懂,我避免在示例源代码中使用错误处理。我将在后面的例子中再次讨论错误处理。
你需要调用的第二个初始化函数是mysql_init()。这个函数在连接到服务器时为您分配MYSQL对象。该函数定义为:
MYSQL *mysql_init(MYSQL *mysql)
下面是使用前面定义的全局变量调用此函数的示例:
mysql = mysql_init(NULL);
注意,我使用了NULL来传递给函数。这是因为这是请求一个新的MYSQL对象实例的函数的第一次调用。在这种情况下,一个新的对象被分配和初始化。如果您调用了传入该对象的现有实例的函数,则该函数只初始化该对象。
如果有错误,函数返回NULL,如果成功,函数返回对象的地址。这意味着您可以将这个调用放在条件语句中,以便在失败时处理错误,或者简单地询问MYSQL指针变量来检测NULL。
提示几乎所有的
mysql_XXX函数成功返回 0,失败返回非零。只有那些返回指针的才返回非零表示成功,返回 0 ( NULL)表示失败。
设置选项
嵌入式服务器允许您在连接到服务器之前设置其他连接选项。用于设置连接选项的函数定义如下:
int mysql_options(MYSQL *mysql, enum mysql_option, const char *arg)
第一个参数是嵌入式服务器对象的实例。第二个参数是可能选项的枚举值,最后一个参数用于为使用可选字符串选择的选项传递参数值。选项列表有一个很长的可能值列表。一些更常用的选项及其值如表 6-3 所示。MySQL 参考手册中列出了完整的选项集。
表 6-3 。连接选项的部分列表
| [计]选项 | 价值 | 描述 |
|---|---|---|
| MYSQL _ OPT _ USE _ REMOTE _ CONNECTION | 不适用的 | 强制连接使用远程服务器进行连接 |
| MYSQL _ OPT _ USE _ EMBEDDED _ CONNECTION | 不适用的 | 强制连接到嵌入式服务器 |
| MYSQL_READ_DEFAULT_GROUP | 组 | 指示服务器从配置文件中的指定组读取服务器配置选项 |
| MYSQL _ SET _ 客户端 _IP | 国际电脑互联网地址 | 为配置为使用身份验证的嵌入式服务器提供 IP 地址 |
以下对此函数的示例调用指示服务器从配置文件的[libmysqld_client]部分读取配置选项,并告诉服务器使用嵌入式连接:
mysql_options(mysql, MYSQL_READ_DEFAULT_GROUP, "libmysqld_client");
mysql_options(mysql, MYSQL_OPT_USE_EMBEDDED_CONNECTION, NULL);
对于成功,返回值为 0;对于任何无效或具有无效值的选项,返回值为非零。
连接到服务器
现在服务器已经初始化,所有选项都已设置,您可以连接到服务器了。你用来做这件事的函数叫做mysql_real_connect()。它有大量允许微调连接的参数。该函数被声明为;
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实例、定义主机名(IP 地址或完全限定名)的字符串、用户名、密码、要使用的初始数据库的名称、要使用的端口号、要使用的 Unix 套接字,最后是启用特殊客户端行为的标志。有关客户端标志的更多详细信息,请参见 MySQL 参考手册。任何被指定为NULL的参数值将通知函数使用该参数的默认值。以下是对此函数的调用示例,它使用除数据库之外的所有默认值进行连接:
mysql_real_connect(mysql, NULL, NULL, NULL, "information_schema", 0, NULL, 0);
如果成功,该函数返回一个连接句柄,如果失败,则返回NULL。大多数应用不会捕获连接句柄。相反,它们检查NULL的返回值。请注意,我没有使用任何身份验证参数。这是因为默认情况下身份验证是关闭的。如果我在打开身份验证开关的情况下编译嵌入式服务器,就必须提供这些参数。最后,第四个参数是您想要连接的默认数据库的名称。该数据库必须存在,否则您可能会遇到错误。
至此,您应该拥有了设置变量以调用嵌入式服务器、初始化、设置选项和连接到嵌入式服务器所需的所有代码。下面显示了由前面的代码示例表示的这些操作:
#include <stdio.h>
#include <stdlib.h>
#include <stdarg.h>
#include "mysql.h"
MYSQL *mysql; //the embedded server class
MYSQL_RES *results; //stores results from queries
MYSQL_ROW record; //a single row in a result set
static char *server_options[] = {"mysql_test",
"--defaults-file=c:\\mysql_embedded\\my.ini",
"--datadir=c:\\mysql_embedded\\data", NULL };
int num_elements = (sizeof(server_options) / sizeof(char *)) - 1;
static char *server_groups[] = {"libmysqld_server", "libmysqld_client", NULL };
int main(void)
{
mysql_server_init(num_elements, server_options, server_groups);
mysql = mysql_init(NULL);
mysql_options(mysql, MYSQL_READ_DEFAULT_GROUP, "libmysqld_client");
mysql_options(mysql, MYSQL_OPT_USE_EMBEDDED_CONNECTION, NULL);
mysql_real_connect(mysql, NULL, NULL, NULL, "INFORMATION_SCHEMA",
0, NULL, 0);
...
return 0;
}
运行查询
最后,我们谈到了好东西——使数据库系统成为数据库系统的核心:对特定查询的处理。允许您发出查询的函数是mysql_query()函数。该函数被声明为;
int mysql_query(MYSQL *mysql, const char *query)
该函数的参数是MYSQL对象实例和一个包含 SQL 语句的字符串(null 终止)。SQL 语句可以是任何有效的查询,包括数据操作语句(SELECT、INSERT、UPDATE、DELETE、DROP等)。).如果查询产生结果,可以使用方法mysql_store_result()和mysql_fetch_row()将结果绑定到一个指针变量进行访问。如果没有返回结果,结果集将是NULL。
调用此函数来检索服务器上的数据库列表的示例如下:
mysql_query(mysql, " SELECT SCHEMA_NAME FROM INFORMATION_SCHEMA.SCHEMATA;")
如果成功,该函数的返回值为 0,如果失败,返回值为非零。
检索结果
发出查询后,接下来的步骤是获取结果集,并在结果指针变量中存储对结果集的引用。然后,您可以获取下一行(记录)并将其存储在记录结构(恰好是一个命名数组)中。完成这个过程的函数是mysql_store_result()和mysql_fetch_row(),定义为;
MYSQL_RES *mysql_store_result(MYSQL *mysql)
MYSQL_ROW mysql_fetch_row(MYSQL_RES *result)
mysql_store_result()函数接受MYSQL对象作为其参数,并返回最近运行的查询的结果集的实例。如果出现错误或者上一次查询没有返回任何结果,该函数将返回NULL。此时,您必须注意通过调用mysql_errno()函数来检查错误。如果有错误,您必须调用错误函数,并将结果与已知错误列表进行比较。该函数生成的已知错误值有CR_OUT_OF_MEMORY(没有存储结果的可用内存)、CR_SERVER_GONE_ERROR或CR_SERVER_LOST(与服务器的连接丢失),以及CR_UNKNOWN_ERR(一个指示服务器处于不可预测状态的总括错误)。
注意使用
mysql_store_result()功能有许多可能的情况。这里描述了最常见的用法。要更详细地了解该函数的用法,或者如果您在诊断使用该函数的问题时遇到问题,请参阅 MySQL 参考手册了解更多详细信息。
mysql_fetch_row()函数接受结果集作为唯一的参数。如果结果集中没有更多的行,函数返回NULL。这很方便,因为它允许您在循环或迭代器中使用这个特性。如果该函数失败,仍然设置NULL的返回值。由您来检查mysql_errno()功能,看看是否发生了任何已定义的错误。这些错误包括指示连接失败的CR_SERVER_LOST,以及无处不在的“出错”错误指示器CR_UNKOWN_ERROR。
这些调用一起用于查询表并将结果打印到控制台的示例有:
mysql_query(mysql, "SELECT ItemNum, Description FROM tblTest");
results = mysql_store_result(mysql);
while(record=mysql_fetch_row(results))
{
printf("%s\t%s\n", record[0], record[1]);
}
注意,在查询运行之后,我调用了mysql_store_result()函数来获得结果;然后,我将mysql_fetch_row()函数放在我的循环评估中。由于mysql_fetch_row()在没有更多行可用时返回NULL(在记录集的末尾),循环将在该点终止。当有行时,我使用数组下标(从 0 开始)访问行中的每一列。
这个例子演示了对嵌入式服务器的所有查询的基本结构。您可以包装这个过程,并将其包含在一个类或一组抽象的函数中。我在第二个嵌入式应用示例中演示了这一点。
清除
从查询返回并放入结果集中的数据需要分配资源。因为我们是优秀的程序员,所以我们努力释放不再需要的内存以避免内存泄漏。 3 Oracle 提供了 mysql_free_result()函数来帮助释放那些资源。该功能定义为:
void mysql_free_result(MYSQL_RES *result)
这个函数是调用安全的,这意味着您可以使用已经释放的结果集调用它,而不会产生错误。这只是以防你高兴起来,开始到处扔“自由”代码。别笑——我见过“免费”电话比“新”电话多的程序。大多数情况下这不是问题,但是如果免费调用使用不当,过多的免费调用可能会释放一些您不想释放的东西。与新操作一样,您应该谨慎使用 free 操作。
以下是调用此函数释放结果集的示例:
mysql_free_result(results);
断开与服务器的连接并完成服务器
当您使用完嵌入式服务器后,您需要断开并关闭它。这可以通过使用 mysql_close()和 mysql_server_end() 4 函数来实现。close 函数关闭连接,另一个函数终结服务器并释放内存。这些功能定义如下:
void mysql_close(MYSQL *mysql);
void mysql_server_end();
这些函数的调用示例如下所示。请注意,这些是您需要进行的最后一次函数调用,通常在关闭应用时调用。
mysql_close(mysql);
mysql_server_end();
把这一切放在一起
现在,让我们一起来看看这些代码。清单 6-1 显示了一个完整的嵌入式服务器,它列出了可以从给定的数据目录访问的数据库。我将在后面的小节中介绍构建和运行这个示例的过程。
注下面的例子是为 Windows 写的。一个 Linux 示例将在后面的章节中讨论。
清单 6-1 。 嵌入式服务器应用实例
#include <stdio.h>
#include <stdlib.h>
#include <stdarg.h>
#include "mysql.h"
MYSQL *mysql; //the embedded server class
MYSQL_RES *results; //stores results from queries
MYSQL_ROW record; //a single row in a result set
static char *server_options[] = {"mysql_test",
"--defaults-file=c:\\mysql_embedded\\my.ini",
"--datadir=c:\\mysql_embedded\\data", NULL };
int num_elements = (sizeof(server_options) / sizeof(char *)) - 1;
static char *server_groups[] = {"libmysqld_server", "libmysqld_client", NULL };
int main(void)
{
mysql_server_init(num_elements, server_options, server_groups);
mysql = mysql_init(NULL);
mysql_options(mysql, MYSQL_READ_DEFAULT_GROUP, "libmysqld_client");
mysql_options(mysql, MYSQL_OPT_USE_EMBEDDED_CONNECTION, NULL);
mysql_real_connect(mysql, NULL, NULL, NULL, "information_schema",
0, NULL, 0);
mysql_query(mysql, "SHOW DATABASES"); // issue query
results = mysql_store_result(mysql); // get results
printf("The following are the databases supported:\n");
while(record=mysql_fetch_row(results)) // fetch row
{
printf("%s\n", record[0]); // process row
}
mysql_query(mysql, "CREATE DATABASE testdb1;");
mysql_query(mysql, "SHOW DATABASES;"); // issue query
results = mysql_store_result(mysql); // get results
printf("The following are the databases supported:\n");
while(record=mysql_fetch_row(results)) // fetch row
{
printf("%s\n", record[0]); // process row
}
mysql_free_result(results);
mysql_query(mysql, "DROP DATABASE testdb1;"); // issue query
mysql_close(mysql);
mysql_server_end();
return 0;
}
错误处理
您可能想知道在前一章中读到的所有错误处理都发生了什么。这些工具在 C API 中。Oracle 提供了两个函数来处理错误。第一个是msyql_errno(),从最近的错误中检索错误号。第二个是mysql_error(),检索最近错误的相关错误消息。这些功能定义如下:
unsigned int mysql_errno(MYSQL *mysql)
const char *mysql_error(MYSQL *mysql)
为这两个函数传递的参数是MYSQL对象。因为这些方法是错误处理程序,所以它们不会失败。然而,如果在没有错误发生时调用它们,mysql_errno()返回 0,mysql_error()返回一个空字符串。
以下是对这些函数的一些调用示例:
if(somethinggoeshinkyhere)
{
printf("There was an error! Error number : %d = $s\n",
mysql_errno(&mysql), mysql_error(&mysql));
}
咻!这就是全部了。我希望我的解释能澄清参考手册中的迷雾。我写这一节主要是因为我觉得没有任何像样的例子可以帮助您学习如何使用嵌入式服务器——至少没有一个例子能在短短的几页中说明需要什么。
构建嵌入式 MySQL 应用
前几节向您介绍了嵌入式 MySQL 应用中使用的基本函数。本节将向您展示如何实际构建一个。我首先向您展示如何编译应用,然后讨论构建嵌入式库调用的方法。我还提供了两个示例应用,供您在自己的系统中进行实验。
我还简要介绍了对核心 MySQL 源代码的修改。是的,我知道这可能有点吓人,但我会一步一步地告诉你所有的细节。幸运的是,这是一个简单的修改,只需要更改两个文件。
我鼓励你阅读我包含的源代码。我知道有很多,但我已经把它精简到我认为可以控制的范围。通过阅读 MySQL 源代码,我学到了很多有趣的东西。我的目标是,通过研究这些示例的源代码,您可以获得构建自己的嵌入式 MySQL 应用的更多见解。
编译库(libmysqld )
在使用嵌入式库(libmysqld)之前,您需要编译它。MySQL 二进制文件的一些发行版可能不包括预编译的嵌入式库。嵌入式库包含在大多数源代码发行版中,可以在源代码树根下的/libmysqld目录中找到。该库通常是在没有调试信息的情况下构建的。您将希望有一个支持调试的版本用于您的开发。
在 Linux 上编译 libmysqld
要在 Linux 下编译这个库,使用configure脚本设置配置,然后执行一个普通的make和make install步骤。您将需要的配置参数是--with-debug和--with-embedded-server。下面显示了完整的过程。从源代码目录的根目录运行。编译过程可能需要一段时间,所以您可以在继续阅读的同时开始编译。编译可能需要几分钟到大约一个小时的时间,这取决于机器的速度以及之前是否用调试信息构建了系统。
注意以下命令构建服务器并将其安装到默认位置。这些操作需要 root 权限。
cmake . -DWITH_EMBEDDED_SERVER=ON -DWITH_DEBUG=ON
make
sudo make install
提示您也可以使用图形界面中的
cmake-gui .命令来设置参数。一旦设置好选项,点击Configure and,然后点击Generate。
在 Windows 上编译 libmysqld
要在 Windows 下编译库,请启动 Visual Studio 并打开根源代码目录中的主解决方案文件(mysql.sln)。打开调试只是选择libmysqld项目并将构建配置设置为Debug Win32。您可以用通常的方式编译这个库,首先在当前项目中点击选择它,然后选择 Build Build 或者构建完整的解决方案。任何依赖项目都将根据需要构建。编译过程可能需要一段时间,所以您可以在继续阅读的同时开始编译。根据机器的速度以及之前是否已经使用调试信息构建了系统,编译可能需要几分钟到半小时的时间。
调试呢?
您可能想知道在嵌入式库中调试是否和在独立服务器中一样。的确如此。事实上,您可以使用相同的调试方法。在运行时调试嵌入式服务器有点困难,但是因为服务器应该是嵌入式的,所以不太可能需要调试到那个级别。为了帮助调试应用,您可能需要创建一个跟踪文件。
我在上一章解释了几种调试技术。DBUG 包是最强大和最容易使用的包之一。虽然嵌入式服务器已经连接了所有的管道,并且确实遵循了标记所有函数入口和出口的相同调试实践,但是 DBUG 包并没有通过嵌入式库公开。
您可以创建自己的 DBUG 包实例,并使用它来编写自己的跟踪文件。对于使用嵌入式服务器的大型应用,您可以选择这样做。大多数应用都很小,所以增加的工作没有什么帮助。在这种情况下,如果嵌入式库提供了调试选项,那就太酷了。
DBUG 包既可以通过配置文件打开,也可以通过直接调用嵌入式库打开。当然,这假设您的嵌入式库是在启用调试的情况下编译的。
在运行时打开跟踪文件需要调用嵌入式库。方法是mysql_debug(),,它采用一个指定调试选项的字符串参数。下面的示例在运行时打开跟踪文件,指定更常用的选项并指示库将跟踪文件写入根目录。应该在连接到服务器之前调用此方法。
mysql_debug("debug=d:t:i:O,\\mysqld_embedded.trace");
提示为你的嵌入式服务器跟踪使用不同的文件名。这将有助于将嵌入式服务器跟踪与您可能运行的任何其他独立服务器区分开来。
您也可以使用配置文件打开调试。只需将前一个例子中的字符串放入您的源代码在启动时指定的my.cnf (my.ini)文件中(稍后会详细介绍)。
如果您想从嵌入式应用中使用 DBUG 包,但不想在自己的代码中包含 DBUG 包,该怎么办?你只是运气不好吗?嵌入式库没有公开 DBUG 方法,但是它可以!以下段落解释了修改嵌入式服务器以包含简单 DBUG 方法的过程。我用一个简单的例子,因为我还不想让你陷入困境。
你需要做的第一件事是备份原始源代码。如果你下载了一个焦油或压缩文件,你没事。如果您发现自己在添加了一些代码后还在努力让服务器编译,那么回到最初的副本会对您的压力水平(和理智)产生深远的影响。如果您已经删除了更改,但仍然无法编译,这一点尤其正确!
添加一个新方法真的很简单。编辑/include目录中的mysql.h文件并添加定义。我选择创建一个公开DBUG_PRINT函数的方法。我把它简单地命名为mysql_dbug_print()。清单 6-2 显示了这个方法的函数定义。请注意,该函数接受单个字符指针。我用它来传入我在嵌入式应用中定义的字符串。这允许我向跟踪文件中写入一个字符串,作为我的嵌入式应用与来自嵌入式服务器的跟踪同步的某种标记。
***清单 6-2 。*对 mysql.h 的修改
/* BEGIN CAB MODIFICATION */
/* Reason for Modification: */
/* Adds a method to permit embedded applications to call DBUG_PRINT */
void STDCALL mysql_dbug_print(const char *a);
/* END CAB MODIFICATION */
要创建函数,编辑/libmysqld/libmysqld.c并将函数添加到源代码的其余部分。位置并不重要,只要它在源代码主体的某个地方。我选择将它放在其他公开的库函数附近(第 89 行附近)。清单 6-3 显示了这个方法的代码。注意,代码只是将字符串回显到了DBUG_PRINT方法中。请注意,我还在传递的字符串末尾添加了一个字符串。这有助于我定位来自我的应用的所有跟踪行,而不管我传递什么来打印。
清单 6-3 。 对 libmysqld.c 的修改
/* BEGIN CAB MODIFICATION */
/* Reason for Modification: */
/* Adds a method to permit embedded applications to call DBUG_PRINT */
void STDCALL mysql_dbug_print(const char *a)
{
DBUG_PRINT(a, (" -- Embedded application."));
}
/* END CAB MODIFICATION */
要将方法添加到 Windows 中的嵌入式库中,您还必须修改libmysqld_exports.def文件以包含新方法。清单 6-4 显示了一个简化的清单作为例子。这里,我已经将mysql_dbug_print()语句添加到文件中。请注意,该文件是按字母顺序维护的。
清单 6-4 。 对 libmysqld_exports.def 的修改
LIBRARY LIBMYSQLD
DESCRIPTION 'MySQL 5.6 Embedded Server Library'
VERSION 5.6
EXPORTS
_dig_vec_upper
_dig_vec_lower
...
mysql_dbug_print
mysql_debug
mysql_dump_debug_info
mysql_eof
...
就这样!现在只需重新编译嵌入式服务器,您的新方法就可以在应用中使用了。我已经对我的嵌入式服务器安装完成了这一步。下面的示例使用此方法将字符串写入跟踪文件。这极大地帮助了我在跟踪文件中找到与我的源代码同步的点。
提示在前面的清单中,我使用了我在第 3 章中介绍的相同的注释策略。这将有助于您在需要迁移到新版本时识别与源代码的任何差异。
那的数据呢?
在开始创建和运行您的第一个嵌入式 MySQL 应用之前,请考虑您想要使用的数据。如果您计划创建一个嵌入式应用,该应用提供一个管理界面,允许您创建表格并填充它们,那么您就万事俱备了。如果您还没有计划这样的接口或类似的设施,那么您将需要使用其他工具来配置数据库。
幸运的是,只要使用较简单的表类型(比如 MyISAM),就可以使用独立的服务器和您喜欢的实用程序来创建数据库和表并填充它们。如果您使用 InnoDB,您应该使用--innodb_file_per_table选项启动服务器,或者创建 MySQL 的全新安装,添加您的数据,然后将数据目录和 InnoDB 文件复制到新位置。创建数据后,您可以将目录从单机服务器安装的数据目录复制到另一个位置。请记住,将嵌入式服务器数据位置与独立服务器数据位置分开非常重要。记下您放置数据的位置,因为您的嵌入式应用将需要这些数据。
我在我所有的例子和我自己的嵌入式应用中使用了这种技术。它让我能够形成和填充我想首先使用的数据,而不必担心创建管理界面。大多数嵌入式 MySQL 应用都是这样构建的。
创建一个基本的嵌入式服务器
前面几节向您展示了使用嵌入式库所需的所有必要功能。我向您展示了一个简单的例子,它使用了我描述的所有函数。我包含了一个 Linux 和 Windows 的例子。虽然它们几乎完全相同,但在源代码中还是有一些细微的差别。最大的区别在于程序是如何编译的。本章中的示例假设您正在使用一个已经编译了调试信息的嵌入式库。
示例程序读取嵌入式服务器的数据目录中的数据库列表,将列表打印到控制台,创建名为testdb1的新数据库,再次读取数据库列表,将列表打印到控制台,最后删除数据库testdb1。虽然不太复杂,但所有示例函数调用都得到了练习。我还包含了打开跟踪文件(DBUG)的调用,以及使用嵌入式库中新的mysql_dbug_print()函数将信息打印到跟踪文件的调用。
Linux 示例
您需要创建的第一个文件是配置文件(my.cnf)。您可以使用现有的配置文件,但是我建议将它复制到您的嵌入式服务器的位置。例如,如果您创建了一个名为/var/lib/mysql_embedded的目录,您将把配置文件放在那里,并将所有数据目录(数据库文件和文件夹)也复制到那个目录中。那些是唯一需要在那个目录中的文件。唯一的例外是,如果您想为您的嵌入式服务器使用不同的语言。在这种情况下,我建议将适当的文件从独立安装复制到您的嵌入式服务器目录中,并从配置文件中引用它们。清单 6-5 显示了示例程序的配置文件。
***清单 6-5 。***Linux 版 my.cnf 文件示例
[libmysqld_server]
basedir=/var/lib/mysql_embedded
datadir=/var/lib/mysql_embedded
#slow query log#=
#tmpdir#=
#port=3306
#set-variable=key_buffer=16M
[libmysqld_client]
#debug=d:t:i:O,\\mysqld_embedded.trace
注意,我已经禁用了大多数选项(通过使用行首的符号)。我通常这样做,以便在需要的时候可以轻松快速地打开它们。调试是关闭的,这样我可以向您展示如何通过编程方式打开它。
您需要创建的下一个文件是应用的源代码。如果您已经按照前面的 C API 教程进行了学习,它应该看起来非常熟悉。清单 6-6 显示了一个简单的嵌入式 MySQL 应用的完整源代码。
清单 6-6 。 嵌入示例 1 (Linux: example1_linux.c)
#include <stdio.h>
#include <stdlib.h>
#include <stdarg.h>
#include "mysql.h"
MYSQL *mysql; //the embedded server class
MYSQL_RES *results; //stores results from queries
MYSQL_ROW record; //a single row in a result set
/*
These variables set the location of the ini file and data stores.
*/
static char *server_options[] = {"mysql_test",
"--defaults-file=/var/lib/mysql_embedded/my.cnf",
"--datadir=/var/lib/mysql_embedded", NULL };
int num_elements = (sizeof(server_options) / sizeof(char *)) - 1;
static char *server_groups[] = {"libmysqld_server", "libmysqld_client", NULL };
int main(void)
{
/*
This section initializes the server and sets server options.
*/
mysql_server_init(num_elements, server_options, server_groups);
mysql = mysql_init(NULL);
mysql_options(mysql, MYSQL_READ_DEFAULT_GROUP, "libmysqld_client");
mysql_options(mysql, MYSQL_OPT_USE_EMBEDDED_CONNECTION, NULL);
/*
The following call turns debugging on programmatically.
Comment out to turn off debugging.
*/
//mysql_debug("d:t:i:O,\\mysqld_embedded.trace");
/*
Connect to embedded server.
*/
mysql_real_connect(mysql, NULL, NULL, NULL, "information_schema",
0, NULL, 0);
/*
This section executes the following commands and demonstrates
how to retrieve results from a query.
SHOW DATABASES;
CREATE DATABASE testdb1;
SHOW DATABASES;
DROP DATABASE testdb1;
*/
mysql_dbug_print("Showing databases."); //record trace
mysql_query(mysql, "SHOW DATABASES;"); //issue query
results = mysql_store_result(mysql); //get results
printf("The following are the databases supported:\n");
while(record=mysql_fetch_row(results)) //fetch row
{
printf("%s\n", record[0]); //process row
}
mysql_dbug_print("Creating the database testdb1."); //record trace
mysql_query(mysql, "CREATE DATABASE testdb1;");
mysql_dbug_print("Showing databases.");
mysql_query(mysql, "SHOW DATABASES;"); //issue query
results = mysql_store_result(mysql); //get results
printf("The following are the databases supported:\n");
while(record=mysql_fetch_row(results)) //fetch row
{
printf("%s\n", record[0]); //process row
}
mysql_free_result(results);
mysql_dbug_print("Dropping database testdb1."); //record trace
mysql_query(mysql, "DROP DATABASE testdb1;"); //issue query
/*
Now close the server connection and tell server we're done (shutdown).
*/
mysql_close(mysql);
mysql_server_end();
return 0;
}
我添加了一些注释(有些人会说是多余的)来帮助您理解代码。我做的第一件事是创建全局变量并设置初始化数组。然后,我用数组选项初始化服务器,再设置几个选项,并连接到服务器。示例应用的主体从数据库中读取数据并打印出来。该示例的最后一部分关闭并终结服务器。
在编译这个例子时,您可以使用mysql_config脚本来标识库的位置。该脚本向命令行返回传递给它的每个选项的实际路径。您也可以从命令行运行该脚本,并查看所有选项及其值。编译该示例的示例命令是:
gcc example1_linux.c -g -o example1_linux -lstdc++ -I./include -L./lib -lmysqld -lpthread -ldl -lcrypt -lm -lrt
这个命令应该适用于大多数 Linux 系统,但是在某些情况下,这可能是一个问题。如果您的 MySQL 安装在另一个位置,您可能需要用mysql_config脚本修改这个短语。如果您的系统上安装了多个 MySQL,或者您在另一个位置安装了嵌入式库,您可能无法使用mysql_config脚本,因为它将返回错误的库路径。对于安装了多个版本的 MySQL 源代码的情况也是如此。您当然希望避免使用一个版本的服务器的包含文件来编译另一个版本的嵌入式库。如果你没有早期的glibc库,你也会遇到问题。
注意如果您正在从源代码树编译嵌入式服务器,并且您正在使用 mysql_config,那么您必须设置 cmake 选项-DCMAKE_INSTALL_PREFIX=ON。
要纠正这些问题,首先从命令行运行mysql_config脚本,并记下库的路径。您还应该找到要使用的库和头文件的正确路径。下面是我如何克服这些问题的一个例子(我在我的 SUSE 机器上遇到了所有这些情况):
g++ example1_linux.c -g -o example1_linux -lz -I/usr/include/mysql
-L/usr/lib/mysql -lmysqld -lz -lpthread -lcrypt -lnsl -lm -lpthread -lc
-lnss_files -lnss_dns -lresolv -lc -lnss_files -lnss_dns -lresolv -lrt
注意,我使用了更新的g++编译器,而不是普通的gcc。这是因为我的系统有最新的 GNU 库,没有旧的。当然,我可以加载旧的库并修复这个问题,但是输入g++要容易得多。好吧,所以我们程序员很懒。
清单 6-7 显示了在 MySQL 的典型安装下运行这个例子的输出示例。在本例中,我将独立服务器目录中的所有数据复制到我的嵌入式服务器目录中。
清单 6-7 。 样本输出
linux:/home/Chuck/source/Embedded # ./example1_linux
The following are the databases supported:
information_schema
mysql
test
The following are the databases supported:
information_schema
mysql
test
testdb1
linux:/home/Chuck/source/Embedded #
请花一些时间在您自己的机器上研究这个示例应用。我建议您对应用的主体进行试验,并运行一些自己的查询,以获得如何编写自己的嵌入式 MySQL 应用的感觉。如果您在嵌入式库中实现了mysql_dbug_print()函数,那么通过删除对mysql_debug()函数调用的注释或者删除配置文件中对debug选项的注释,在示例中尝试一下。
下一个例子将向您展示如何封装嵌入的库调用;它展示了它们在更现实的应用中的用途。
Windows 示例
您需要创建的第一个文件是配置文件(my.ini)。您可以使用现有的配置文件,但是我建议将它复制到您的嵌入式服务器的位置。例如,如果您创建了一个名为c:/mysql_embedded的目录,那么您应该将配置文件放在那里,并将所有数据目录也复制到该目录中。那些是唯一需要在那个目录中的文件。唯一的例外是,如果您想为您的嵌入式服务器使用不同的语言。在这种情况下,我建议将适当的文件从独立安装复制到您的嵌入式服务器目录中,并从配置文件中引用它们。清单 6-8 显示了示例程序的配置文件。包括最常用的选项以及它们在文件中的指定位置。
***清单 6-8 。***Windows 版示例 my.ini 文件
[mysqld]
basedir=C:/mysql_embedded
datadir=C:/mysql_embedded/data
language=C:/mysql_embedded/share/english
[libmysqld_client]
#debug=d:t:i:O,\\mysqld_embedded.trace
创建项目文件有点复杂。为了充分利用 Visual Studio,我建议从源代码目录的根目录打开主解决方案文件(mysql.sln ),并将您的新应用作为新项目添加到该解决方案中。您不必将源代码存储在同一个源代码树中,但是您应该以这样一种方式存储它,以便知道它适用于哪个版本的源代码。
您可以使用项目向导创建项目。应该选择 C++ Win32 控制台项目模板(Visual Studio 2012 中的 Templates
Visual c++
Win32)并给项目命名。这将在向导中指定的文件夹的根目录下创建一个与项目同名的新文件夹。您应该创建一个空项目并添加您自己的源文件。
创建一个项目文件作为解决方案的子项目会给你带来一些非常棒的优势。利用自动化构建过程(没有生成文件——耶!),将libmysqld项目添加到项目依赖项中。你可以从项目的项目依赖菜单中打开项目依赖工具。使用解决方案的配置下拉框将生成配置设置为活动(调试),并使用标准工具栏上的解决方案的平台下拉框将平台设置为活动(Win32)。
您还需要在项目属性中设置一些开关。通过选择项目属性或右键单击项目并选择属性
.打开项目-属性对话框。您要检查的第一项是运行时库生成。通过展开树中的 C/C++标签,单击树中的代码生成标签,并从运行时库下拉列表中选择它,将此开关设置为多线程调试 DLL (/MDd)。此选项使您的应用使用特定于调试多线程和 DLL 的运行时库版本。图 6-1 显示了项目属性对话框和该选项的位置。
图 6-1。项目属性对话框,显示代码生成页面
接下来,将 MySQL include 目录添加到项目属性中。最简单的方法是展开 C/C++标签并单击命令行标签。这将显示命令行参数。要添加新参数,请在附加选项文本框中键入它。在这种情况下,您需要添加一个选项,例如:
/I ../include。
如果您的项目不在 MySQL 源代码树下,您可能需要相应地修改参数。图 6-2 显示了项目属性对话框和该选项的位置。
图 6-2。项目属性对话框:命令行页面
如果不想(或不需要)使用预编译头,也可以移除预编译头选项。此选项位于“项目属性”对话框中的“C/C++预编译头”页上。
使用项目添加引用菜单选项将 libmysqld 项目添加到 example1_win32 项目。将打开一个对话框(如图
6-3 所示),允许您在解决方案中选择一个项目作为参考。选择 libmysqld 项目。图 6-4 显示了添加了 libmysqld 项目引用的项目属性对话框。不执行这一步将导致编译时出现大量未定义的符号错误。
图 6-3。选择一个项目参考
图 6-4。项目属性对话框:参照
现在您已经正确配置了项目,如果您在创建项目时选择了创建基本项目文件,请添加您的源文件或粘贴示例代码。清单 6-9 显示了完整的 Windows 版本。
清单 6-9。 Embedded Example 1 (Windows: example1_win32.cpp)
#include <stdio.h>
#include <stdlib.h>
#include <stdarg.h>
#include "mysql.h"
MYSQL *mysql; //the embedded server class
MYSQL_RES *results; //stores results from queries
MYSQL_ROW record; //a single row in a result set
/*
These variables set the location of the ini file and data stores.
*/
static char *server_options[] = {"mysql_test",
"--defaults-file=c:\\mysql_embedded\\my.ini",
"--datadir=c:\\mysql_embedded\\data", NULL };
int num_elements = (sizeof(server_options) / sizeof(char *)) - 1;
static char *server_groups[] = {"libmysqld_server", "libmysqld_client", NULL };
int main(void)
{
/*
This section initializes the server and sets server options.
*/
mysql_server_init(num_elements, server_options, server_groups);
mysql = mysql_init(NULL);
mysql_options(mysql, MYSQL_READ_DEFAULT_GROUP, "libmysqld_client");
mysql_options(mysql, MYSQL_OPT_USE_EMBEDDED_CONNECTION, NULL);
/*
The following call turns debugging on programmatically.
Comment out to turn off debugging.
*/
mysql_debug("d:t:i:O,\\mysqld_embedded.trace");
/*
Connect to embedded server.
*/
mysql_real_connect(mysql, NULL, NULL, NULL, "information_schema",
0, NULL, 0);
/*
This section executes the following commands and demonstrates
how to retrieve results from a query.
SHOW DATABASES;
CREATE DATABASE testdb1;
SHOW DATABASES;
DROP DATABASE testdb1;
*/
mysql_dbug_print("Showing databases."); //record trace
mysql_query(mysql, "SHOW DATABASES;"); //issue query
results = mysql_store_result(mysql); //get results
printf("The following are the databases supported:\n");
while(record=mysql_fetch_row(results)) //fetch row
{
printf("%s\n", record[0]); //process row
}
mysql_dbug_print("Creating the database testdb1."); //record trace
mysql_query(mysql, "CREATE DATABASE testdb1;");
mysql_dbug_print("Showing databases.");
mysql_query(mysql, "SHOW DATABASES;"); //issue query
results = mysql_store_result(mysql); //get results
printf("The following are the databases supported:\n");
while(record=mysql_fetch_row(results)) //fetch row
{
printf("%s\n", record[0]); //process row
}
mysql_free_result(results);
mysql_dbug_print("Dropping database testdb1."); //record trace
mysql_query(mysql, "DROP DATABASE testdb1;"); //issue query
/*
Now close the server connection and tell server we’re done (shutdown).
*/
mysql_close(mysql);
mysql_server_end();
return 0;
}
我添加了一些注释(有些人会说是多余的)来帮助您理解代码。我做的第一件事是创建全局变量并设置初始化数组。然后,我用数组选项初始化服务器,如果需要,再设置几个选项,并连接到服务器。示例应用的主体从数据库中读取数据并打印出来。该示例的最后一部分关闭并终结服务器。
编译这个例子非常简单。只需选择项目,然后选择项目构建,或者右键单击项目并选择构建。如果你已经编译了
libmysqld项目,你应该看到的只是例子的编译。如果出于某种原因,目标文件对于libmysqld或它的任何依赖项来说都是过期的,Visual Studio 也会编译这些文件。
注意你可能会在
mysql_com.h或者类似的头文件中遇到一些非常奇怪的错误。最有可能的原因是优化策略。微软自动将#define WIN32_LEAN_AND_MEAN语句包含在stdafx.h文件中。如果你打开了它,它会告诉编译器忽略一些不需要的包含和链接(正常情况下)。您可能希望将这一行完全删除(或者将其注释掉)。你的程序现在应该编译没有错误。如果您选择不使用stdafx文件,您应该不会遇到这个问题。
编译完成后,您可以从“调试”菜单命令运行程序,或者打开命令窗口并从命令行运行程序。如果这是您第一次使用,您应该会看到如下错误消息:
This application has failed to start because LIBMYSQLD.dll was not found.
Re-installing the application may fix this problem.
这个错误的原因与错误消息中的第二句话无关。这意味着嵌入库不在搜索路径中。如果你曾经和。NET 或 COM 应用,并且从未使用过 C 库,您可能从未遇到过该错误。不像。NET 和 COM、C 库没有在全局程序集缓存(GAC)或注册表中注册。这些库(dll)应该与调用它们的应用放在一起,或者至少放在一个执行路径上。大多数开发者将 DLL 的副本放在执行目录中。
要解决这个问题,请将libmysqld.dll文件从lib_debug目录复制到example1_win32.exe文件所在的目录(或将lib_debug添加到执行路径)。一旦你越过了这个障碍,你应该会看到类似于清单 6-10 所示的输出。
清单 6-10。 示例输出
c:\source\mysql-5.6\example1_win32\Debug>example1_win32
The following are the databases supported:
information_schema
cluster
mysql
test
The following are the databases supported:
information_schema
cluster
mysql
test
testdb1
请花一些时间在您自己的机器上研究这个示例应用。我建议您对应用的主体进行实验,并运行一些自己的查询,以获得如何编写自己的嵌入式 MySQL 应用的感觉。如果您在嵌入式库中实现了mysql_dbug_print()函数,那么通过删除对mysql_debug()函数调用的注释或者删除配置文件中对debug选项的注释,在示例中尝试一下。
错误处理呢?
你们中的一些人可能想知道错误处理。具体来说,如何检测嵌入式服务器的问题并妥善处理它们?许多嵌入式库调用都有错误代码,您可以对其进行查询和操作。前面几节描述了我将使用的函数的返回值。虽然我在第一个嵌入式 MySQL 示例中没有包括太多的错误处理,但我会在下一个示例中介绍。请注意我是如何捕获错误并处理向客户端发送错误的。
嵌入式服务器应用
前面的例子展示了如何创建一个基本的嵌入式 MySQL 应用。虽然这些示例展示了如何连接和读取专用 MySQL 安装中的数据,但它们并不是构建您自己的嵌入式应用的好模型,因为除了最琐碎的需求之外,它们没有足够的覆盖面。哦,他们没有任何错误处理!这一章中的例子虽然是虚构的,但都是为了给你提供构建一个真正的嵌入式应用所需的工具。
这个应用被称为 售书机(BVM),是一个嵌入式系统,旨在运行在基于微软 Windows 的专用 PC 上,带有触摸屏。该系统及其其他输入设备安装在专门用于分发书籍的机械售货机中。BVM 背后的想法是允许出版商以半移动包的形式提供他们最受欢迎的图书,供应商可以根据需要进行配置和补充。BVM 将允许出版商在空间有限的地方安装他们的自动售货机,如贸易展览、机场和购物中心。这些地区通常有大量对购买印刷书籍感兴趣的顾客。BVM 减少了对店面和员工的需求,从而为出版商节省了资金。
注我经常发现自己怀疑这个想法是否被考虑过。我读过几篇预测按需印刷持续增长的文章,但我很少看到任何关于售书机如何工作的文章。我知道一些出版商已经安装了一些原型,但是这些试验并没有产生太多的热情。我选择用这个例子来增加一些真实性。我也阅读技术书籍,并经常发现自己对不切实际或琐碎的例子感到厌烦。这里有一个例子,我希望你同意至少是可信的。
界面
该应用需要一个双 接口;一个用于正常的自动售货机活动,另一个允许供应商重新进货自动售货机,根据需要调整信息。自动售货机界面旨在为客户提供一组按钮,这些按钮为自动售货机中的特定插槽提供图书缩略图。由于大多数现代自动售货机使用产品按钮,当产品可用时,按钮被照亮,当产品耗尽时,按钮变暗或关闭,BVM 界面在该槽中的产品可用时启用按钮,当产品耗尽时禁用按钮。
当顾客点击一个产品按钮时,屏幕变成一个简短、详细的显示,描述这本书并给出它的价格。如果顾客想购买这本书,她可以点击购买,并提示付款。这个应用就是用来模拟这些活动的。一个真正的实现将调用适当的硬件控制库来接收付款,验证付款,并使用自动售货机的机械部分来从指定的插槽分发产品。图 6-5 显示了售书机的主界面。图 6-6 显示了一些书籍的低数量的影响。
图 6-5。售书机客户界面
图 6-6。产生客户界面的“产品耗尽”视图
图 6-7 显示了其中一本书的细节示例。
图 6-7。图书详情界面
如果没有任何补充产品的方法,自动售货机就不会很有用。BVM 通过管理界面提供这种功能。当供应商需要补充书籍或更改细节以匹配不同的书籍集时,供应商打开机器并关闭嵌入式应用(该功能必须添加到示例中)。然后,供应商将重启应用,在命令行上提供管理员开关,如下所示:
C:\>Books BookVendingMachine -admin
管理界面允许供应商输入特定查询并执行它。图 6-8 显示了管理界面。该示例显示了重置产品数量的典型更新操作。该接口允许供应商输入她需要的任何查询来为嵌入式应用重置数据。
图 6-8。管理界面
数据和数据库
本例中的 数据是在独立的 MySQL 服务器上创建的,并被复制到嵌入式 MySQL 目录中。当我创建这个应用时,我首先设计了数据结构和数据库来保存数据。这总是一个好主意。
注意有些开发者可能不同意,他们认为最好从用户界面设计开始,让数据需求不断发展。这两种做法都不比另一种好。重要的一点是,数据必须是设计的重点。
您的大多数项目都将带有对现有存储库中的数据或实际数据的需求。对于新的应用,比如这个例子,总是通过设计表来设计数据库,以表示项目和它们之间的关系。这通常是小型项目中的一个步骤,但也可能是一个迭代过程,在这个过程中,您使用初始表和关系作为输入,使用 UML 绘图和建模技术来设计和规划用户界面。对数据库(数据的组织)的更改通常是在后面的步骤中发现的,然后您可以将这些更改用作再次经历该过程的起点。
本例中的数据由一个简短的列表组成,该列表是关于机器中书籍的描述性字段。这包括标题、作者、价格和描述。我添加了 ISBN 作为表的键(因为它在定义上是唯一的,并且被出版业用作标识图书的主要方式)。我还添加了一些其他领域,我想看看,然后再决定购买一本书。这些包括出版日期和页数。我还需要存储一个缩略图。(我选择了外部方法,将路径和文件名存储到文件中,并从文件系统中读取它。我本来可以使用二进制大对象(BLOB)来存储缩略图,但是这样更容易——尽管不可否认容易出错。)最后,我计划了运行用户界面所需的内容,并决定添加一个字段来记录图书所在和分发的位置编号,以及一个字段来测量现有数量。我将该表命名为books,并将其放在名为bvm的数据库中。这里显示了该表的CREATE SQL 语句。清单 6-11 显示了使用EXPLAIN命令的表格布局。
CREATE DATABASE BVM;
CREATE TABLE Books (
ISBN varchar(15) NOT NULL,
Title varchar(125) NOT NULL,
Authors varchar(100) NOT NULL,
Price float NOT NULL,
Pages int NOT NULL,
PubDate date NOT NULL,
Quantity int DEFAULT 0,
Slot int NOT NULL,
Thumbnail varchar(100) NOT NULL,
Description text NOT NULL
);
清单 6-11。 表结构
mysql> USE bvm
mysql> explain Books;
+-----------------+------------------+--------+-----+--- -----+--------+
| Field | Type | Null | Key | Default | Extra |
+-----------------+------------------+--------+-----+---------+--------+
| ISBN | varchar(15) | NO | | | |
| Title | varchar(125) | NO | | | |
| Authors | varchar(100) | NO | | | |
| Price | float | NO | | | |
| Pages | int(11) | NO | | | |
| PubDate | date | NO | | | |
| Quantity | int(11) | YES | | 0 | |
| Slot | int(11) | NO | | | |
| Thumbnail | varchar(100) | NO | | | |
| Description | text | NO | | | |
+-----------------+------------------+--------+-----+---------+--------+
10 rows in set (0.08 sec)
为了管理缩略图,我将缩略图文件名存储在缩略图字段中,并对路径使用系统级选项。一种方法是创建一个命令行开关。另一种方法是将它放在 MySQL 配置文件中并从那里读取。也可以从数据库中读取。我使用了一个名为settings的数据库表,它只包含两个字段:FieldName,存储选项的名称(例如"ImagePath"),以及Value,存储它的值(例如"c:\images\mypic.tif")。这个方法允许我创建任意数量的系统选项,并从外部控制它们。此处显示了用于settings表的CREATE SQL 命令,后面是一个示例INSERT命令,用于设置示例应用的ImagePath选项:
CREATE TABLE settings (FieldName varchar(20), Value varchar(255));
INSERT INTO settings VALUES ("ImagePath", "c:\\mysql_embedded\\images\\");
创建项目
创建项目的最佳方式是使用向导创建新的 Windows 项目。我建议从源代码目录的根目录打开主解决方案文件,并将您的新应用作为新项目添加到该解决方案中。您不必将您的源代码存储在同一个源代码树中,但是您应该以这样一种方式存储它,以便知道它适用于哪个版本的源代码。
您可以使用项目向导创建项目。选择 CLR Windows 窗体应用项目模板,并将项目命名为。这将在向导中指定的文件夹的根目录下创建一个与项目同名的新文件夹。
创建一个项目文件作为解决方案的子项目会给你带来一些非常棒的优势。利用自动化的构建过程(不用生成文件——好极了!),您需要将libmysqld项目添加到项目的依赖项中。您可以从项目项目依赖菜单中打开项目依赖工具。使用解决方案的配置下拉框将生成配置设置为活动(调试),并使用标准工具栏上的解决方案的平台下拉框将平台设置为活动(Win32)。
您还需要在项目属性中设置一些开关。打开“项目属性”对话框。首先要检查的是运行时库生成。通过展开树中的 C/C++标签,单击树中的代码生成标签,并从运行时库下拉列表中选择它,将此开关设置为多线程调试 DLL (/MDd)。[本章前面的图 6-1](#Fig1) 显示了项目属性对话框和这个选项的位置。
然后,将 MySQL include 目录添加到项目属性中。最简单的方法是展开 C/C++标签并单击命令行标签。这将显示命令行参数。要添加新参数,请在附加选项文本框中键入它。在这种情况下,你需要添加类似/I ../include`的东西。如果您的项目不在 MySQL 源代码树下,您可能需要相应地修改参数。图 6-2 本章前面显示了项目属性对话框和这个选项的位置。
如果不想(或不需要)使用预编译头,也可以移除预编译头选项。此选项位于“项目属性”对话框中的“C/C++预编译头”页上。
和前面的例子一样,您还需要使用 Project->Add Reference 菜单项添加 libmysqld 项目作为引用。图 6-3 和 6-4 描述了该操作的对话框。
最后,将公共语言运行时设置设为/clr。您可以在“项目属性”对话框中进行设置,方法是在树中单击“常规”,然后从“公共语言运行时支持”选项中选择“公共语言运行时支持(/clr)”。图 6-9 显示了项目对话框和该选项的位置。
图 6-9。项目属性对话框:常规页面
设计
我必须满足两个重要的需求来设计应用。我不仅需要设计一个易于使用且没有错误的用户界面,还需要能够从. NET 应用中调用 C API。如果你在 MySQL 论坛和列表中做一些搜索,你会看到一些可怜的人在努力让它工作。如果你跟随我的例子,你应该不会遇到那些问题。问题的主要原因似乎是无法调用嵌入式库中的 C API 函数。我通过使用托管 C++代码用 C++编写我的应用来解决这个问题。您不能在托管应用中使用 C API 调用,但是 C++允许您通过使用#pragma unmanaged和#pragma managed指令暂时关闭和重新打开它。
调用非托管代码的需求也是封装库调用的一个巨大动力。非托管代码使开发者能够编写一个可以在不是用. NET 编写的程序中使用的 DLL。对于这个例子,我使用一个 C++类来封装包装在#pragma unmanaged指令中的 C API 调用。这允许我向您展示一个直接调用嵌入式库 C API 的. NET 应用的示例。酷吧。
我还想让用户界面完全独立于任何与嵌入式库有关的东西。我想这样做,这样我就可以为您提供一个封装的数据库访问类,您可以重用它作为您自己的应用的基础。它还允许我向您展示一个真实应用的例子(Windows ),而不需要您通读长长的源代码列表。因此,本例的数据访问设计是一个单一的非托管 C++类,它封装了嵌入式库 C API 调用。该设计还包括两个表单:每个用户界面对应一个表单(Customer和Administrator)。
托管与非托管代码
托管代码是。在公共语言运行库(CLR)控制下运行的. NET 应用。这些应用可以利用 CLR 的所有功能,特别是垃圾回收和更好的程序执行控制。非托管代码是不在 CLR 下运行的 Windows 应用,因此不能从。净增强。
数据库引擎类
我开始设计数据库引擎类时只使用了纸和笔。我本来可以使用 UML 绘图应用,但是因为这个类很小,所以我只列出了我需要的方法。例如,我需要初始化、连接和关闭嵌入式 MySQL 服务器的方法。这些方法很容易封装,因为它们不需要表单中的任何参数。
我遇到的第一个挑战是错误处理。我如何在不要求客户端了解任何关于嵌入库的信息的情况下将错误传达给客户端表单?可能有许多方法可以做到这一点,但是我选择实现一个错误检查方法,它允许客户端在一个操作之后检查是否存在错误,然后使用另一个方法来检索错误消息。这允许我再次将数据库访问与表单分开。
与发出查询和检索结果有关的类方法是从选择的实现中设计出来的。我选择实现一个访问迭代器,它允许客户机发出查询,然后遍历结果。我还需要一种方法来告诉数据库一本书已经售出,这样数据库就可以减少这本书的现有量。
数据检索是使用三种方法完成的,它们返回一个字符串、一个整数或一个大的文本字段。我还添加了 helper 方法,用于从settings表中获取设置,从数据库中获取字段(用于管理员界面),以及检索现有数量的快速方法。
清单 6-12 显示了数据库类头的完整源代码。我给这个班取名为DBEngine。表 6-4 包含了对类中每个方法的描述和使用。
清单 6-12 。数据库引擎类头(DBEngine.h)
#pragma once
#pragma unmanaged
#include <stdio.h>
class DBEngine
{
private:
bool mysqlError;
public:
DBEngine(void);
const char *GetError();
bool Error();
void Initialize();
void Shutdown();
char *GetSetting(char *Field);
char *GetBookFieldStr(int Slot, char *Field);
char *GetBookFieldText(int Slot, char *Field);
int GetBookFieldInt(int Slot, char *Field);
int GetQty(int Slot);
void VendBook(char *ISBN);
void StartQuery(char *QueryStatement);
void RunQuery(char *QueryStatement);
int GetNext();
char *GetField(int fldNum);
∼DBEngine(void);
};
#pragma managed
表 6-4 。数据库引擎类方法
| 方法 | 返回 | 描述 |
|---|---|---|
| GetError() | 字符* | 返回生成的最后一个错误的错误消息。 |
| 错误() | (同 Internationalorganizations)国际组织 | 如果服务器检测到错误情况,则返回 1。 |
| 初始化() | 空的 | 封装嵌入式服务器初始化和连接操作。 |
| 关机() | 空的 | 封装嵌入式服务器终止和关闭操作。 |
| GetSetting() | 字符* | 返回名为的设置的值。在设置表中查找信息。 |
| GetBookFieldStr() | 字符* | 从 books 表中为指定槽中传递的字段返回一个字符串值。 |
| GetBookFieldText() | 字符* | 从 books 表中为指定槽中传递的字段返回一个字符串值。 |
| GetBookFieldInt() | (同 Internationalorganizations)国际组织 | 从 books 表中为在指定槽中传递的字段返回一个整数值。 |
| 获取数量() | (同 Internationalorganizations)国际组织 | 返回指定槽中图书的现有数量。 |
| VendBook() | 空的 | 减少指定插槽中图书的现有数量。 |
| 开始查询() | 空的 | 通过执行查询并检索结果集来初始化查询迭代器。 |
| RunQuery() | 空的 | 一种帮助器方法,用于运行不返回结果的查询。 |
| 下一步() | (同 Internationalorganizations)国际组织 | 检索结果集中的下一条记录。如果结果集中没有更多记录,则返回 0;如果成功,则返回非零值。 |
| 盖菲尔德 | 字符* | 返回传递的字段编号的字段名。 |
定义类是容易的部分。完成所有这些方法的代码有点困难。我没有从头开始,而是使用了第一个示例中的代码,并将其更改为数据库类的源代码。清单 6-13 显示了数据库类的完整源代码。请注意,我在初始化和启动选项中使用了相同的全局(对这个源代码来说是本地)变量和字符数组。这部分你应该很熟悉。花些时间通读这段代码。当你完成后,我会解释一些更具体的细节。
清单 6-13。 数据库引擎类(DBEngine.cpp)
#pragma unmanaged
#include "DBEngine.h"
#include <stdio.h>
#include <stdlib.h>
#include <stdarg.h>
#include "mysql.h"
MYSQL *mysql; //the embedded server class
MYSQL_RES *results; //stores results from queries
MYSQL_ROW record; //a single row in a result set
bool IteratorStarted; //used to control iterator
MYSQL_RES *ExecQuery(char *Query);
/*
These variables set the location of the ini file and data stores.
*/
static char *server_options[] = {"mysql_test",
"--defaults-file=c:\\mysql_embedded\\my.ini",
"--datadir=c:\\mysql_embedded\\data", NULL };
int num_elements = (sizeof(server_options) / sizeof(char *)) - 1;
static char *server_groups[] = {"libmysqld_server", "libmysqld_client", NULL };
DBEngine::DBEngine(void)
{
mysqlError = false;
}
DBEngine::∼DBEngine(void)
{
}
const char *DBEngine::GetError()
{
return (mysql_error(mysql));
mysqlError = false;
}
bool DBEngine::Error()
{
return(mysqlError);
}
char *DBEngine::GetBookFieldStr(int Slot, char *Field)
{
char *istr = new char[10];
char *str = new char[128];
_itoa_s(Slot, istr, 10, 10);
strcpy_s(str, 128, "SELECT ");
strcat_s(str, 128, Field);
strcat_s(str, 128, " FROM books WHERE Slot = ");
strcat_s(str, 128, istr);
mysqlError = false;
results=ExecQuery(str);
strcpy_s(str, 128, "");
if (results)
{
mysqlError = false;
record=mysql_fetch_row(results);
if(record)
{
strcpy_s(str, 128, record[0]);
}
else
{
mysqlError = true;
}
}
return (str);
}
char *DBEngine::GetBookFieldText(int Slot, char *Field)
{
char *istr = new char[10];
char *str = new char[128];
_itoa_s(Slot, istr, 10, 10);
strcpy_s(str, 128, "SELECT ");
strcat_s(str, 128, Field);
strcat_s(str, 128, " FROM books WHERE Slot = ");
strcat_s(str, 128, istr);
mysqlError = false;
results=ExecQuery(str);
delete str;
if (results)
{
mysqlError = false;
record=mysql_fetch_row(results);
if(record)
{
return (record[0]);
}
else
{
mysqlError = true;
}
}
return ("");
}
int DBEngine::GetBookFieldInt(int Slot, char *Field)
{
char *istr = new char[10];
char *str = new char[128];
int qty = 0;
_itoa_s(Slot, istr, 10, 10);
strcpy_s(str, 128, "SELECT ");
strcat_s(str, 128, Field);
strcat_s(str, 128, " FROM books WHERE Slot = ");
strcat_s(str, 128, istr);
results=ExecQuery(str);
if (results)
{
record=mysql_fetch_row(results);
if(record)
{
qty = atoi(record[0]);
}
else
{
mysqlError = true;
}
}
delete str;
return (qty);
}
void DBEngine::VendBook(char *ISBN)
{
char *str = new char[128];
char *istr = new char[10];
int qty = 0;
strcpy_s(str, 128, "SELECT Quantity FROM books WHERE ISBN = '");
strcat_s(str, 128, ISBN);
strcat_s(str, 128, "'");
results=ExecQuery(str);
record=mysql_fetch_row(results);
if (record)
{
qty = atoi(record[0]);
if (qty >= 1)
{
_itoa_s(qty - 1, istr, 10, 10);
strcpy_s(str, 128, "UPDATE books SET Quantity = ");
strcat_s(str, 128, istr);
strcat_s(str, 128, " WHERE ISBN = '");
strcat_s(str, 128, ISBN);
strcat_s(str, 128, "'");
results=ExecQuery(str);
}
}
else
{
mysqlError = true;
}
}
void DBEngine::Initialize()
{
/*
This section initializes the server and sets server options.
*/
mysql_server_init(num_elements, server_options, server_groups);
mysql = mysql_init(NULL);
if (mysql)
{
mysql_options(mysql, MYSQL_READ_DEFAULT_GROUP, "libmysqld_client");
mysql_options(mysql, MYSQL_OPT_USE_EMBEDDED_CONNECTION, NULL);
/*
The following call turns debugging on programmatically.
Comment out to turn off debugging.
*/
//mysql_debug("d:t:i:O,\\mysqld_embedded.trace");
/*
Connect to embedded server.
*/
if(mysql_real_connect(mysql, NULL, NULL, NULL, "INFORMATION_SCHEMA",
0, NULL, 0) == NULL)
{
mysqlError = true;
}
else
{
mysql_query(mysql, "use bvm");
}
}
else
{
mysqlError = true;
}
IteratorStarted = false;
}
void DBEngine::Shutdown()
{
/*
Now close the server connection and tell server we're done (shutdown).
*/
mysql_close(mysql);
mysql_server_end();
}
char *DBEngine::GetSetting(char *Field)
{
char *str = new char[128];
strcpy_s(str, 128, "SELECT * FROM settings WHERE FieldName = '");
strcat_s(str, 128, Field);
strcat_s(str, 128, "'");
results=ExecQuery(str);
strcpy_s(str, 128, "");
if (results)
{
record=mysql_fetch_row(results);
if (record)
{
strcpy_s(str, 128, record[1]);
}
}
else
{
mysqlError = true;
}
return (str);
}
void DBEngine::StartQuery(char *QueryStatement)
{
if (!IteratorStarted)
{
results=ExecQuery(QueryStatement);
if (results)
{
record=mysql_fetch_row(results);
}
}
IteratorStarted=true;
}
void DBEngine::RunQuery(char *QueryStatement)
{
results=ExecQuery(QueryStatement);
if (results)
{
record=mysql_fetch_row(results);
if(!record)
{
mysqlError = true;
}
}
}
int DBEngine::GetNext()
{
//if EOF then no more records
IteratorStarted=false;
record=mysql_fetch_row(results);
if (record)
{
return (1);
}
else
{
return (0);
}
}
char *DBEngine::GetField(int fldNum)
{
if (record)
{
return (record[fldNum]);
}
else
{
return ("");
}
}
MYSQL_RES *ExecQuery(char *Query)
{
mysql_dbug_print("ExecQuery.");
mysql_free_result(results);
mysql_query(mysql, Query);
return (mysql_store_result(mysql));
}
#pragma managed
关于这段代码,您应该注意到的一点是,我添加了所有的错误处理,以使代码更加健壮,或者说更加坚固。虽然我没有实现所有可能的错误处理程序,但最重要的是。
get 方法都是使用相同的过程实现的。我首先生成适当的查询(从而对客户机隐藏 SQL 语句),执行查询,检索结果集,然后从查询中检索记录,并返回值。
一个有趣的方法是VendBook()。花点时间再看一遍。您将看到,我使用了类似的方法生成查询,但是这次我没有得到结果,因为没有任何结果。实际上,有一个结果—它是受影响的记录的数量。如果您想在应用中进行一些额外的过程或规则检查,这可能会很方便。
其余的方法对您来说应该很熟悉,因为它们都是我向您展示的原始示例的副本,只是这次它们包含了错误处理。现在,让我们看看用户界面代码如何调用数据库类。
客户界面(主表单)
客户界面的源代码非常大。这是因为微软将自动生成的代码放在了form.h文件中。我只包括我写的那些部分。我包含了这一部分,向您展示如何编写自己的程序。NET(或其他)用户界面。除了按钮事件中的代码,我只使用了完成用户界面所需的四个附加方法。第一种方法DisplayError()定义为:
void DisplayError()
我使用这个函数来检测数据库类中的错误,并向用户显示错误消息。该方法的实现是对MessageBox::Show()函数的典型调用。
第二个方法是一个辅助方法,它完成所选书籍的详细视图。这个函数被命名为LoadDetails()。我抽象了这个方法,因为我意识到我将为所有十个按钮重复代码。以这种方式抽象可以最大限度地减少代码并允许更容易的调试。该方法定义为:
void LoadDetails(int Slot)
该方法将插槽号(对应于按钮号)作为参数。它使用数据库类方法查询数据库,并填充细节界面元素。这是与数据库引擎类通信的大部分繁重工作发生的地方。
注意你可能想知道字符串周围那些错综复杂的代码是什么。事实证明。NET 字符串类与 C 样式的字符串不兼容。我包含的额外代码旨在在这些格式之间封送字符串。
第三个方法是名为Delay()的帮助器方法,定义为:
void Delay(int secs)
该函数导致处理延迟,延迟时间为作为参数传递的秒数。虽然这不是您希望包含在自己的应用中的东西,但我还是添加了它来模拟售货过程。这是一个很好的例子,说明如何使用存根功能来演示应用。这在构建新界面的原型时尤其有用。
第四种方法CheckAvailability(),用于根据是否有足够数量的产品可用来打开或关闭界面上的按钮。该方法定义为:
void CheckAvailability()
该函数对数据库引擎进行一系列调用,以检查每个插槽的数量。如果插槽是空的(quantity == 0),按钮被禁用。
清单 6-14 显示了客户界面源代码的摘录。我省略了大量自动生成的代码(表示为. . .)。注意,在文件的顶部,我使用了#include "DBEngine.h"指令来引用数据库引擎头。还要注意,我定义了一个DBEngine类型的变量。我在整个代码中使用这个对象。因为它是表单的局部变量,所以我可以在任何事件或方法中使用它。我使用. . .来表示清单中省略的自动生成代码和注释部分。
清单 6-14。 主窗体源代码(MainForm.h)
#include <stdio.h>
#include <stdlib.h>
#include <string>
#include "vcclr.h"
#include <time.h>
#include "DBEngine.h"
namespace BookVendingMachine {
const char GREETING[] = "Please make a selection.";
DBEngine *Database = new DBEngine();
. . .
#pragma endregion
void DisplayError()
{
String ^str = gcnew String("There was an error with the database system.\n" \
"Please contact product support.\nError = ");
str = str + gcnew String(Database->GetError());
MessageBox::Show(str, "Internal System Error", MessageBoxButtons::OK,
MessageBoxIcon::Information);
}
void LoadDetails(int Slot)
{
int Qty = Database->GetBookFieldInt(Slot, "Quantity");
if (Database->Error()) DisplayError();
pnlButtons->Visible = false;
pnlDetail->Visible = true;
lblStatus->Visible = false;
lblTitle->Text = gcnew String(Database->GetBookFieldStr(Slot, "Title"));
if (Database->Error()) DisplayError();
lblAuthors->Text =
gcnew String(Database->GetBookFieldStr(Slot, "Authors"));
if (Database->Error()) DisplayError();
lblISBN->Text = gcnew String(Database->GetBookFieldStr(Slot, "ISBN"));
if (Database->Error()) DisplayError();
txtDescription->Text =
gcnew String(Database->GetBookFieldText(Slot, "Description"));
if (Database->Error()) DisplayError();
lblPrice->Text = gcnew String(Database->GetBookFieldStr(Slot, "Price"));
if (Database->Error()) DisplayError();
lblNumPages->Text =
gcnew String(Database->GetBookFieldStr(Slot, "Pages"));
if (Database->Error()) DisplayError();
lblPubDate->Text =
gcnew String(Database->GetBookFieldStr(Slot, "PubDate"));
if (Database->Error()) DisplayError();
if(Qty < 1)
{
btnPurchase->Enabled = false;
}
}
void CheckAvailability()
{
btnBook1->Enabled = (Database->GetBookFieldInt(1, "Quantity") >= 1);
if (Database->Error()) DisplayError();
btnBook2->Enabled = (Database->GetBookFieldInt(2, "Quantity") >= 1);
if (Database->Error()) DisplayError();
btnBook3->Enabled = (Database->GetBookFieldInt(3, "Quantity") >= 1);
if (Database->Error()) DisplayError();
btnBook4->Enabled = (Database->GetBookFieldInt(4, "Quantity") >= 1);
if (Database->Error()) DisplayError();
btnBook5->Enabled = (Database->GetBookFieldInt(5, "Quantity") >= 1);
if (Database->Error()) DisplayError();
btnBook6->Enabled = (Database->GetBookFieldInt(6, "Quantity") >= 1);
if (Database->Error()) DisplayError();
btnBook7->Enabled = (Database->GetBookFieldInt(7, "Quantity") >= 1);
if (Database->Error()) DisplayError();
btnBook8->Enabled = (Database->GetBookFieldInt(8, "Quantity") >= 1);
if (Database->Error()) DisplayError();
btnBook9->Enabled = (Database->GetBookFieldInt(9, "Quantity") >= 1);
if (Database->Error()) DisplayError();
btnBook10->Enabled = (Database->GetBookFieldInt(10, "Quantity") >= 1);
if (Database->Error()) DisplayError();
}
void Delay(int secs)
{
time_t start;
time_t current;
time(&start);
do
{
time(¤t);
} while(difftime(current,start) < secs);
}
private: System::Void btnCancel_Click(System::Object^ sender,
System::EventArgs^ e)
{
lblStatus->Visible = true;
pnlDetail->Visible = false;
pnlButtons->Visible = true;
btnPurchase->Enabled = true;
lblStatus->Text = gcnew String(GREETING);
}
private: System::Void btnPurchase_Click(System::Object^ sender,
System::EventArgs^ e)
{
String ^orig = gcnew String(lblISBN->Text->ToString());
pin_ptr<const wchar_t> wch = PtrToStringChars(orig);
// Convert to a char*
size_t origsize = wcslen(wch) + 1;
const size_t newsize = 100;
size_t convertedChars = 0;
char nstring[newsize];
wcstombs_s(&convertedChars, nstring, origsize, wch, _TRUNCATE);
lblStatus->Visible = true;
pnlDetail->Visible = false;
pnlButtons->Visible = true;
btnPurchase->Enabled = true;
Database->VendBook(nstring);
//
// Simulate buying the book.
//
lblStatus->Text = "Please Insert your credit card.";
this->Refresh();
Delay(3);
lblStatus->Text = "Thank you. Processing card number ending in 4-1234.";
this->Refresh();
Delay(3);
lblStatus->Text = "Vending....";
this->Refresh();
Delay(5);
this->Refresh();
CheckAvailability();
lblStatus->Text = gcnew String(GREETING);
}
private: System::Void MainForm_Load(System::Object^ sender,
System::EventArgs^ e)
{
String ^imageName;
String ^imagePath;
Database->Initialize();
if (Database->Error()) DisplayError();
//
//For each button, check to see if there are sufficient qty and load
//the thumbnail for each.
//
imagePath = gcnew String(Database->GetSetting("ImagePath"));
imageName = imagePath +
gcnew String(Database->GetBookFieldStr(1, "Thumbnail"));
if (Database->Error()) DisplayError();
btnBook1->Image = btnBook1->Image->FromFile(imageName);
imageName = imagePath +
gcnew String(Database->GetBookFieldStr(2, "Thumbnail"));
if (Database->Error()) DisplayError();
btnBook2->Image = btnBook2->Image->FromFile(imageName);
imageName = imagePath +
gcnew String(Database->GetBookFieldStr(3, "Thumbnail"));
if (Database->Error()) DisplayError();
btnBook3->Image = btnBook3->Image->FromFile(imageName);
imageName = imagePath +
gcnew String(Database->GetBookFieldStr(4, "Thumbnail"));
if (Database->Error()) DisplayError();
btnBook4->Image = btnBook4->Image->FromFile(imageName);
imageName = imagePath +
gcnew String(Database->GetBookFieldStr(5, "Thumbnail"));
if (Database->Error()) DisplayError();
btnBook5->Image = btnBook5->Image->FromFile(imageName);
imageName = imagePath +
gcnew String(Database->GetBookFieldStr(6, "Thumbnail"));
if (Database->Error()) DisplayError();
btnBook6->Image = btnBook6->Image->FromFile(imageName);
imageName = imagePath +
gcnew String(Database->GetBookFieldStr(7, "Thumbnail"));
if (Database->Error()) DisplayError();
btnBook7->Image = btnBook7->Image->FromFile(imageName);
imageName = imagePath +
gcnew String(Database->GetBookFieldStr(8, "Thumbnail"));
if (Database->Error()) DisplayError();
btnBook8->Image = btnBook8->Image->FromFile(imageName);
imageName = imagePath +
gcnew String(Database->GetBookFieldStr(9, "Thumbnail"));
if (Database->Error()) DisplayError();
btnBook9->Image = btnBook9->Image->FromFile(imageName);
imageName = imagePath +
gcnew String(Database->GetBookFieldStr(10, "Thumbnail"));
if (Database->Error()) DisplayError();
btnBook10->Image = btnBook10->Image->FromFile(imageName);
CheckAvailability();
}
private: System::Void btnBook1_Click(System::Object^ sender,
System::EventArgs^ e)
{
LoadDetails(1);
}
private: System::Void btnBook2_Click(System::Object^ sender,
System::EventArgs^ e)
{
LoadDetails(2);
}
private: System::Void btnBook3_Click(System::Object^ sender,
System::EventArgs^ e)
{
LoadDetails(3);
}
private: System::Void btnBook4_Click(System::Object^ sender,
System::EventArgs^ e)
{
LoadDetails(4);
}
private: System::Void btnBook5_Click(System::Object^ sender,
System::EventArgs^ e)
{
LoadDetails(5);
}
private: System::Void btnBook6_Click(System::Object^ sender,
System::EventArgs^ e)
{
LoadDetails(6);
}
private: System::Void btnBook7_Click(System::Object^ sender,
System::EventArgs^ e)
{
LoadDetails(7);
}
private: System::Void btnBook8_Click(System::Object^ sender,
System::EventArgs^ e)
{
LoadDetails(8);
}
private: System::Void btnBook9_Click(System::Object^ sender,
System::EventArgs^ e)
{
LoadDetails(9);
}
private: System::Void btnBook10_Click(System::Object^ sender,
System::EventArgs^ e)
{
LoadDetails(10);
}
private: System::Void MainForm_FormClosing(System::Object^ sender,
System::Windows::Forms::FormClosingEventArgs^ e)
{
Database->Shutdown();
}
};
}
MainForm_Load()事件是数据库引擎初始化和按钮加载适当缩略图的地方。我用语句跟踪对数据库的每个调用。
if (Database->Error()) DisplayError();
这个语句允许我检测错误发生的时间并通知用户。虽然我不会在这个事件中对错误采取行动,但我可以在其他事件中对它采取行动。如果这里出现严重的数据库错误,最坏的情况是按钮不会被缩略图填充。我在整个源代码中使用这个概念。
实现btnBook1_Click()到btnBook10_Click()事件来调用LoadDetails()方法,并用适当的数据填充细节接口组件。如您所见,抽象细节的加载为我节省了大量代码!
在界面的细节部分有两个按钮。btnCancel_Click()事件将界面返回到初始的自动售货机视图。btnPurchase_Click()事件更有趣一点。这是贩卖部分发生的地方。注意,我首先调用了VendBook()方法,然后运行自动售货过程的模拟,并将界面返回到自动售货视图。
就这样!客户界面非常简单,就像大多数自动售货机一样。只有一排按钮和一个收钱的机制(在这种情况下,我假设机器接受信用卡支付,但真正的自动售货机可能会采取几种支付形式)。
管理界面(管理表单)
客户界面简单易用。但是如何维护数据呢?供应商如何补充自动售货机的库存,甚至改变提供的图书列表?一种方法是使用独立于客户界面的管理界面。您还可以创建另一个单独的嵌入式应用来处理这个问题,或者可能在另一台机器上创建数据并复制到自动售货机。我选择构建一个简单的管理表单,如图 6-10 所示。
图 6-10。示例给药形式
与客户界面一样,我需要创建一个助手函数。函数LoadList(),用于填充一个列表,该列表显示了books表中的所有数据。这很方便,因为它允许供应商查看数据库包含的内容。
清单 6-15 显示了管理表单源代码的摘录。我省略了自动生成的 Windows 窗体代码(表示为. . .)。在源代码的顶部,我将指针变量定义为AdminDatabase而不是Database。这主要是为了清楚起见,并不意味着分散您对数据库引擎类的使用的注意力。我使用. . .来表示清单中省略的自动生成代码和注释部分。
清单 6-15。 管理表单源代码(AdminForm.h)
#pragma once
#include "DBEngine.h"
using namespace System;
using namespace System::ComponentModel;
using namespace System::Collections;
using namespace System::Windows::Forms;
using namespace System::Data;
using namespace System::Drawing;
namespace BookVendingMachine {
DBEngine *AdminDatabase = new DBEngine();
. . .
#pragma endregion
void LoadList()
{
int i = 0;
int j = 0;
String^ str;
lstData->Items->Clear();
AdminDatabase->StartQuery("SELECT ISBN, Slot, Quantity, Price," \
" Pages, PubDate, Title, Authors, Thumbnail," \
" Description FROM books");
do
{
str = gcnew String("");
for (i = 0; i < 10; i++)
{
if (i != 0)
{
str = str + "\t";
}
str = str + gcnew String(AdminDatabase->GetField(i));
}
lstData->Items->Add(str);
j++;
}while(AdminDatabase->GetNext());
}
private: System::Void btnExecute_Click(System::Object^ sender,
System::EventArgs^ e)
{
String ^orig = gcnew String(txtQuery->Text->ToString());
pin_ptr<const wchar_t> wch = PtrToStringChars(orig);
// Convert to a char*
size_t origsize = wcslen(wch) + 1;
const size_t newsize = 100;
size_t convertedChars = 0;
char nstring[newsize];
wcstombs_s(&convertedChars, nstring, origsize, wch, _TRUNCATE);
AdminDatabase->RunQuery(nstring);
LoadList();
}
private: System::Void Admin_Load(System::Object^ sender,
System::EventArgs^ e)
{
AdminDatabase->Initialize();
LoadList();
}
private: System::Void AdminForm_FormClosing(System::Object^ sender,
System::Windows::Forms::FormClosingEventArgs^ e)
{
AdminDatabase->Shutdown();
}
};
}
我在表单 load 和 closing 事件中包含了对数据库引擎的初始化和关闭方法调用。
这个接口被设计成接受一个特别的查询,并在点击Execute按钮时执行它。因此,btnExecute_Click()是这个源代码中唯一的其他方法。该方法调用数据库引擎并请求运行查询,但不检查任何结果。那是因为这个界面是用来调整数据库中的东西,而不是选择数据。这个方法中的最后一个调用是LoadList()助手方法,它重新填充列表。
检测接口请求
您可能想知道我打算如何检测要执行哪个接口。答案是,我使用命令行参数来告诉代码运行哪个接口。该开关在BookVendingMachine.cpp源文件中的main()函数中实现。处理命令行参数的源代码是不言自明的。清单 6-16 包含嵌入式应用的main()函数的完整源代码。
***清单 6-16。***bookvending machine 主要功能(BookVendingMachine.cpp)
// BookVendingMachine.cpp : main project file.
#include "MainForm.h"
#include "AdminForm.h"
using namespace BookVendingMachine;
[STAThreadAttribute]
int main(array<System::String ^> ^args)
{
// Enabling Windows XP visual effects before any controls are created
Application::EnableVisualStyles();
Application::SetCompatibleTextRenderingDefault(false);
// Create the main window and run it
if ((args->Length == 1) && (args[0] == "-admin"))
{
Application::Run(gcnew AdminForm());
}
else
{
Application::Run(gcnew MainForm());
}
return 0;
}
现在,您应该能够从本文或从图书网站下载信息来重新创建这个示例。我鼓励您熟悉客户机源代码(表单),这样您就可以看到并理解数据库引擎是如何使用的。当你准备好了,你可以编译并运行这个例子。
编译和运行
编译这个例子只需要点击 BuildBuild bookvending machine。如果你已经编译了
libmysqld项目,你应该看到的只是例子的编译。如果出于某种原因,目标文件对于libmysqld或它的任何依赖项来说都是过期的,Visual Studio 也会编译这些文件。
注下面的例子我用的是 Visual Studio 2010。较新版本的 Visual Studio 可能会更改某些菜单命令的位置。所有描述的变化都可以在新版本中设置。
编译完成后,您可以从调试菜单命令运行程序,或者打开一个命令窗口,通过从项目目录输入命令debug\BookVendingMachine从命令行运行程序。如果这是您第一次使用,您应该会看到如下错误消息:
此应用未能启动,因为找不到 LIBMYSQLD.dll。
重新安装应用可能会解决这个问题。
这个错误的原因与错误消息中的第二句话无关。这意味着嵌入库不在搜索路径中。如果你曾经和。NET 或 COM 应用,并且从未使用过 C 库,您可能从未遇到过该错误。不像。NET 和 COM,C 库没有在 GAC 或注册表中注册。这些库(dll)应该与调用它们的应用放在一起,或者至少在一个执行路径上。大多数开发者将 DLL 的副本放在执行目录中。
要解决这个问题,请将libmysqld.dll文件从lib_debug目录复制到bookvendingmachine.exe文件所在的目录(或将lib_debug添加到执行路径)。一旦将库复制到执行目录,您应该看到应用如图 6-3 、 6-4 和 6-5 所示运行。
花些时间,摆弄一下界面。如果时间延迟太烦人,您可以减少延迟的秒数,或者注释掉延迟方法调用。
如果您想访问管理界面,请使用-admin命令行开关运行程序。如果从命令行运行该示例,可以输入命令:
BookVendingMarchine -admin
如果希望使用调试器从 Visual Studio 运行该示例,请在项目属性中设置命令行开关。通过选择项目项目属性打开对话框,并点击树中的调试标签。通过将命令行参数键入 Command Arguments 选项,可以添加任意数量的命令行参数。图 6-11 显示了该选项在项目属性中的位置。
图 6-11。从 Visual Studio 设置命令行参数
我鼓励你尝试这个例子。如果您没有运行 Windows,您仍然可以使用数据库引擎类,并为应用提供自己的接口。这应该不难,因为您已经看到了该接口如何与抽象的libmysqld系统调用一起工作的一个例子。如果您发现自己正在使用嵌入式 MySQL 系统构建独特的自动售货机,请给我发一张照片!
摘要
在本章中,您学习了如何创建嵌入式 MySQL 应用。MySQL embedded library 经常被忽视,但是它非常成功地允许系统集成商向他们的企业应用和产品添加强大的数据管理工具。
也许这一章最吸引人的地方是你对 MySQL 嵌入式库 C API 的引导之旅。我希望通过学习本章中的例子,您能够体会到嵌入式 MySQL 应用的强大功能。我还希望,如果你在编译源代码时遇到了问题,你不会沮丧地扔掉这本书。一个优秀的开源开发者的主要素质是她系统地诊断和调整环境以适应当前项目需求的能力。如果你遇到问题,不要绝望。解决问题是学习周期的自然组成部分。
您还探索了为嵌入式应用打开调试跟踪的概念。我还带您进行了一次简短的旅程,通过嵌入式库公开一个 DBUG 方法来修改 MySQL 服务器源代码,该方法允许您向 DBUG 跟踪输出添加自己的字符串。您看到了一些有趣的错误处理情况以及如何处理它们。最后,我向您展示了一个封装的数据库访问类,您可以在自己的嵌入式应用中使用它。
在下一章,我将研究 MySQL 系统的一个更流行的扩展。这包括添加您自己的用户定义函数(UDF),扩展现有的 SQL 命令,以及向服务器添加您自己的 SQL 命令。这些技术允许 MySQL 系统进一步发展,以满足您的环境的特定需求。
1 嗯,直到现在看来。
2 此功能可改为mysql_library_init()。在撰写本文时,这两种功能都得到支持。
3 它实际上泄漏的并不多,因为它不再被引用,而是仍然被分配,使得那部分内存不可用。
4 这在 MySQL 以后的版本中可能会改为mysql_library_end()。在撰写本文时,这两种功能都得到支持。
我发现 Visual Basic 只有一个特性很酷:控件数组。唉,它们都是过去式了。`
七、向 MySQL 添加函数和命令
系统集成商面临的最大挑战之一是克服被集成系统的局限性。这通常是由于系统对集成有限制,或者没有集成所需的某些功能或命令。通常,这意味着通过创建更多的“粘合”程序来翻译或增强现有的功能和命令来解决问题。
MySQL 开发者认识到了这一需求,并在 MySQL 服务器中添加了灵活的选项来添加新的功能和命令。例如,您可能需要添加函数来执行一些计算或数据转换,或者您可能需要一个新命令来提供特定的管理数据。
本章向您介绍了可用于添加函数的选项,并向您展示了如何向服务器添加您自己的 SQL 命令。我们将探索用户定义的函数、本地函数和新的 SQL 命令。本章的大部分背景材料已经在前面的章节中介绍过了。在你继续学习的时候,请随意查阅这些章节。
添加用户自定义函数
MySQL 支持用户自定义函数(UDF)已经有一段时间了。UDF 是一个新功能(计算、转换等)。),您可以将它添加到服务器中,从而扩展可以在 SQL 命令中使用的可用函数的列表。UDF 最好的一点是它们可以在运行时动态加载。此外,您可以创建自己的 UDF 库,并在您的企业中使用它们,甚至免费提供它们(作为开源)。这可能是系统集成商寻求扩展 MySQL 服务器的第一个地方。MySQL 工程师对 UDF 机制有另一个天才级的想法。
只要 SQL 语言允许使用表达式,就可以在任何地方使用用户定义函数。例如,您可以在存储过程和SELECT语句中使用 UDF。它们是扩展您的服务器而不必修改服务器源代码的极好方法。事实上,您可以定义任意多的 UDF,甚至可以将它们组合在一起形成函数库。每个库都是一个单独的文件,包含编译成库的源代码(Linux 中的.so或 Windows 中的.dll)。
该机制类似于插件接口,事实上,早于插件接口。UDF 接口利用外部的、可动态加载的目标文件来加载和卸载 UDF。该机制使用一个CREATE FUNCTION命令在每个函数的基础上建立到可加载目标文件的连接,并使用一个DROP FUNCTION命令删除函数的连接。让我们来看看这些命令的语法。
创建函数语法
CREATE FUNCTION命令向服务器注册函数,在 mysql.func 表中放置一行。语法是:
CREATE FUNCTION function_name RETURNS [STRING | INTEGER | REAL | DECIMAL] SONAME "mylib.so";
function_name参数代表您正在创建的函数的名称。返回类型可以是STRING、INTEGER、REAL或DECIMAL中的一种,SONAME表示库的名称。CREATE FUNCTION命令告诉 MySQL 服务器创建命令(function_name)中的函数名到目标文件的映射。当调用函数时,服务器调用库中的函数来执行。
DROP 函数语法
DROP FUNCTION命令通过从所选数据库的func表中删除相关的行,向服务器注销该函数。语法如下所示。function_name参数代表您正在创建的函数的名称。
DROP FUNCTION function_name;
让我们看看如何创建一个 UDF 库,并在 MySQL 服务器安装中使用它。我们将从修改现有的示例 UDF 库开始。一旦熟悉了函数的编码方式,创建一个新的源文件并将其添加到服务器构建文件(CMakeLists.txt)是一个基本的练习。
创建用户自定义库
有两种类型的用户定义函数:
- 您可以创建作为单个调用运行的函数,该调用计算一组参数并返回单个结果。
- 您可以创建函数,作为从分组函数中调用的聚合。例如,您可以创建将一种数据类型转换为另一种数据类型的 UDF,例如将日期字段从一种格式更改为另一种格式的函数,或者您可以创建对一组记录执行高级计算的函数,例如平方和函数。UDF 只能返回整数、字符串或实数值。
- 您可以创建提供 SELECT 语句中使用的值的函数。
单呼叫 UDF 是最常见的。它们用于对一个或多个参数执行操作。在某些情况下,不使用任何参数。例如,您可以创建一个 UDF,为全局状态或类似的SERVER_STATUS()返回值。这种形式的 UDF 通常用在SELECT语句的字段列表中,或者作为辅助函数用在存储过程中。
聚合 UDF 函数用于GROUP BY子句中。当它们被使用时,它们在表中的每一行被调用一次,在组的末尾再次被调用。
创建 UDF 库的过程是创建一个新项目,该项目公开 UDF 加载/卸载方法(xxx_init和xxx_deinit,其中xxx是函数的名称)和函数本身。每条语句调用一次xxx_init和xxx_deinit函数。每一行都会调用一次XXX函数。如果您正在创建一个聚合函数,您还需要实现分组函数xxx_clear和xxx_add。调用xxx_clear函数来重置值(在组的开始)。对分组中的每一行调用xxx_add函数,在分组处理结束时调用函数本身。因此,聚合被清除,然后为每个 add 调用添加数据。最后,调用函数本身来返回值。
一旦实现了这些函数,您就可以编译该文件并将其复制到服务器安装的插件目录中。您可以使用CREATE FUNCTION命令加载和使用这些功能。清单 7-1 展示了一组用于 UDF 的示例方法。
清单 7-1 。 样 UDF 战法
/*
Simple example of how to get a sequences starting from the first
argument or 1 if no arguments have been given
*/
my_bool sequence_init(UDF_INIT *initid, UDF_ARGS *args, char *message)
{
if (args->arg_count > 1)
{
strmov(message,"This function takes none or 1 argument");
return 1;
}
if (args->arg_count)
args->arg_type[0]= INT_RESULT; /* Force argument to int */
if (!(initid->ptr=(char*) malloc(sizeof(longlong))))
{
strmov(message,"Couldn't allocate memory");
return 1;
}
memset(initid->ptr, 0, sizeof(longlong));
/*
sequence() is a non-deterministic function : it has different value
even if called with the same arguments.
*/
initid->const_item=0;
return 0;
}
void sequence_deinit(UDF_INIT *initid)
{
if (initid->ptr)
free(initid->ptr);
}
longlong sequence(UDF_INIT *initid __attribute__((unused)), UDF_ARGS *args,
char *is_null __attribute__((unused)),
char *error __attribute__((unused)))
{
ulonglong val=0;
if (args->arg_count)
val= *((longlong*) args->args[0]);
return ++*((longlong*) initid->ptr) + val;
}
Oracle 提供了一个名为udf_example.cc and的示例 UDF 项目,它位于/sql文件夹中,包含您可能想要创建的所有类型的函数的示例。这为添加您自己的函数提供了一个极好的起点。示例函数包括:
- 在字符串上产生类似 soundex 操作的变音函数
- 一个示例函数,返回一个 double 值,该值是参数的字符代码值之和除以所有参数的长度之和
- 示例函数返回一个整数,该整数是参数长度的总和
- 基于传递的值返回序列中下一个值的序列函数
- 从整数参数(数量)和双参数(成本)列表中返回平均成本的示例聚合函数
根据您的需要,您可能会发现其中一些示例很有用。
让我们从修改示例 UDF 项目开始。找到位于源代码根目录下的/sql目录中的udf_example.cc文件,并在您喜欢的编辑器中打开它。因为 udf_example 库包含在 cmake 文件中,所以编译它非常容易。完成编辑后,只需执行 make 即可。在 Windows 上,可以使用 Visual Studio 重新生成 mysql.sln 文件。
警告 Windows 用户必须从库中删除网络 UDF。Windows 不直接支持这些功能。如果遇到关于缺少头文件或外部函数的错误,请注释掉这些函数。
如果在编译过程中遇到错误,请返回并更正它们,然后再次尝试编译。最可能的原因是键入了错误的名称、不正确的代码替换或不正确的路径。
现在库已经编译好了,让我们测试加载和卸载操作。这将确保库已被正确编译并位于正确的位置。打开一个 MySQL 客户端窗口,发出CREATE FUNCTION和DROP FUNCTION命令来加载库中的所有函数。清单 7-2 显示了加载和卸载前五个函数的命令。该列表显示了用于 Windows 的命令;在 Linux 上用udf_example.so替换udf_example.dll。在执行这些函数的任何平台上,输出都是一样的。
清单 7-2 。 样本创建和删除功能命令
CREATE FUNCTION metaphon RETURNS STRING SONAME "udf_example.dll";
CREATE FUNCTION myfunc_double RETURNS REAL SONAME "udf_example.dll";
CREATE FUNCTION myfunc_int RETURNS INTEGER SONAME "udf_example.dll";
CREATE FUNCTION sequence RETURNS INTEGER SONAME "udf_example.dll";
CREATE AGGREGATE FUNCTION avgcost RETURNS REAL SONAME "udf_example.dll";
DROP FUNCTION metaphon;
DROP FUNCTION myfunc_double;
DROP FUNCTION myfunc_int;
DROP FUNCTION sequence;
DROP FUNCTION avgcost;
清单 7-3 和 7-4 显示了运行前面显示的CREATE FUNCTION和DROP FUNCTION命令时的正确结果。
清单 7-3 。?? 安装功能
mysql> CREATE FUNCTION metaphon RETURNS STRING SONAME "udf_example.dll";Query OK, 0 rows affected (0.00 sec)
mysql> CREATE FUNCTION myfunc_double RETURNS REAL SONAME "udf_example.dll";Query OK, 0 rows affected (0.00 sec)
mysql> CREATE FUNCTION myfunc_int RETURNS INTEGER SONAME "udf_example.dll";Query OK, 0 rows affected (0.00 sec)
mysql> CREATE FUNCTION sequence RETURNS INTEGER SONAME "udf_example.dll";Query OK, 0 rows affected (0.00 sec)
mysql> CREATE AGGREGATE FUNCTION avgcost RETURNS REAL SONAME "udf_example.dll";Query OK, 0 rows affected (0.00 sec)
清单 7-4 。?? 卸载功能
mysql> DROP FUNCTION metaphon;Query OK, 0 rows affected (0.00 sec)
mysql> DROP FUNCTION myfunc_double;Query OK, 0 rows affected (0.00 sec)
mysql> DROP FUNCTION myfunc_int;Query OK, 0 rows affected (0.00 sec)
mysql> DROP FUNCTION sequence;Query OK, 0 rows affected (0.00 sec)
mysql> DROP FUNCTION avgcost;Query OK, 0 rows affected (0.00 sec)
现在,让我们运行命令,看看它们是否有效。回到 MySQL 客户端窗口,再次运行CREATE FUNCTION命令来加载 UDF。清单 7-5 展示了库中前五个 UDF 的执行示例。请随意尝试所示的命令。你的结果应该差不多。
清单 7-5 。 执行 UDF 命令的例子
mysql> SELECT metaphon("This is a test.");
+−−---------------------------+
| metaphon("This is a test.") |
+−−---------------------------+
| 0SSTS |
+−−---------------------------+
1 row in set (0.00 sec)
mysql> SELECT myfunc_double(5.5, 6.1);
+−−-----------------------+
| myfunc_double(5.5, 6.1) |
+−−-----------------------+
| 50.17 |
+−−-----------------------+
1 row in set (0.01 sec)
mysql> SELECT myfunc_int(5, 6, 8);
+−−-------------------+
| myfunc_int(5, 6, 8) |
+−−-------------------+
| 19 |
+−−-------------------+
1 row in set (0.00 sec)
mysql> SELECT sequence(8);
+−−-----------+
| sequence(8) |
+−−-----------+
| 9 |
+−−-----------+
1 row in set (0.00 sec)
mysql> CREATE TABLE testavg (order_num int key auto_increment, cost double, qty int);
Query OK, 0 rows affected (0.02 sec)
mysql> INSERT INTO testavg (cost, qty) VALUES (25.5, 17);Query OK, 1 row affected (0.00 sec)
mysql> INSERT INTO testavg (cost, qty) VALUES (0.23, 5);Query OK, 1 row affected (0.00 sec)
mysql> INSERT INTO testavg (cost, qty) VALUES (47.50, 81);Query OK, 1 row affected (0.00 sec)
mysql> SELECT avgcost(qty, cost) FROM testavg;
+−−------------------+
| avgcost(qty, cost) |
+−−------------------+
| 41.5743 |
+−−------------------+
1 row in set (0.03 sec)
最后几个命令展示了avgcost()聚合函数的一个非常基本的用法。在使用GROUP BY子句时,通常会使用聚合函数。
添加新的用户自定义函数
让我们现在添加一个新的 UDF 到库。如果您正在进行一个集成项目,并且需求要求用 Julian 格式表示日期,该怎么办?儒略历转换只是将一年中的某一天(自前一年的 12 月 31 日以来经过的天数)加上年份形成一个数值,如 DDDYYYY。在这种情况下,您需要添加一个函数,该函数接受月份、日期和年份值,并返回以儒略日表示的日期。该功能应定义为:
longlong julian(int month, int day, int year);
我保持了函数的简单,使用了三个整数。该函数可以以多种方式实现(例如,接受日期或字符串值)。现在让我们将JULIAN函数添加到您刚刚构建的 UDF 库中。
这就是创建你自己的 UDF 库的价值所在。任何时候你需要一个新的函数,你都可以把它添加到现有的库中,而不需要从头开始创建一个新的项目。
添加新 UDF 的过程从将函数声明添加到 UDF 库源代码的extern部分开始,然后实现这些函数。然后可以重新编译这个库,并将其部署到 MySQL 服务器安装的插件目录中。让我们用JULIAN函数来完成这个过程。
注意使用
SHOW VARIABLES LIKE 'plugin%';发现插件目录。
打开udf_example.cc文件并添加函数声明。回想一下,您需要定义julian_init()、julian_deinit()和julian()函数。julian_init()函数有三个参数:
UDF_INIT,该方法可以用来在方法之间传递信息的结构UDF_ARGS,一个包含参数数量、参数类型和参数本身的结构- 发生错误时方法应返回的字符串
julian()方法有四个参数:
- 由
julian_init()函数完成的UDF_INIT结构 - 一个包含参数数量、参数类型和参数的
UDF_ARGS结构 - 如果结果为空,则指向设置为 1 的变量的 char 指针
- 发生错误时发送给调用方的消息
julian_deinit()函数使用由julian_init()函数完成的UDF_INIT结构。
当从服务器调用 UDF 时,一个新的UDF_INIT结构被创建并传递给函数,参数被放在UDF_ARGS结构中,然后调用julian_init()函数。如果那个函数没有错误地返回,那么从julian_init()函数中用UDF_INIT结构调用julian()函数。在julian()函数完成后,调用julian_deinit()函数来清除保存在UDF_INIT结构中的值。清单 7-6 显示了添加了JULIAN函数的文件的declaration部分的摘录。该部分用C_MODE_START和C_MODE_END宏表示,位于文件的顶部。我们包含了修改标记,以确保其他人(或者将来的我们自己)知道我们有意修改了这个文件。
清单 7-6 。 朱利安的声明 (udf_example.cc)
C_MODE_START;
...
/* BEGIN CAB MODIFICATION */
/* Reason for Modification: */
/* This section declares the methods for the Julian function */
my_bool julian_init(UDF_INIT *initid, UDF_ARGS *args, char *message);
longlong julian(UDF_INIT *initid, UDF_ARGS *args,
char *is_null, char *error);
void julian_deinit(UDF_INIT *initid);
/* END CAB MODIFICATION */
...
C_MODE_END;
注意我们显示了用椭圆圈出的宏,以指示这些语句应该放在哪里。
现在可以添加这些函数的实现了。我发现复制与我的返回类型匹配的示例函数,然后修改它们以满足我的需要是很有帮助的。julian_init()函数负责初始化变量并检查正确的用法。由于JULIAN函数需要三个整数参数,您需要添加适当的错误处理来实现这一点。清单 7-7 展示了julian_init()功能的实现。您可以在 udf_example.cc 文件的末尾附近插入此方法。
清单 7-7 。 实现为 julian_init()函数(udf_example.cc)
/* BEGIN CAB MODIFICATION */
/* Reason for Modification: */
/* This section implements the Julian initialization function */
my_bool julian_init(UDF_INIT *initid, UDF_ARGS *args, char *message)
{
if (args->arg_count != 3) /* if there are not three arguments */
{
strcpy(message, "Wrong number of arguments: JULIAN() requires 3 arguments.");
return 1;
}
if ((args->arg_type[0] != INT_RESULT) ||
(args->arg_type[1] != INT_RESULT) ||
(args->arg_type[2] != INT_RESULT))
{
strcpy(message, "Wrong type of arguments: JULIAN() requires 3 integers.");
return 1;
}
return 0;
}
/* END CAB MODIFICATION */
注意在清单 7-7 中,首先检查参数计数,然后是三个参数的类型检查。这确保了它们都是整数。精明的程序员会注意到代码还应该检查值的范围。由于代码不检查参数的范围,这可能导致异常或无效的返回值。如果您决定在您的库中实现该功能,我将把它留给您来完成。当参数值的定义域和范围已知时,检查范围值始终是一个好的做法。
实际上并不需要julian_deinit()函数,因为没有内存或变量需要清理。您可以实现一个空函数来完成这个过程。即使你不需要这个函数,编写它也是一个好主意。清单 7-8 展示了这个函数的实现。因为我们没有使用任何新的变量或结构,所以实现只是一个空函数。如果已经创建了变量或结构,您可以在这个函数中释放它们。
清单 7-8 。 实现为 julian_deinit()函数(udf_example.cc)
/* BEGIN CAB MODIFICATION */
/* Reason for Modification: */
/* This section implements the Julian deinitialization function */
void julian_deinit(UDF_INIT *initid)
{
}
/* END CAB MODIFICATION */
JULIAN函数的真正工作发生在julian()实现中。清单 7-9 显示了完整的julian()功能。
注一些复杂的儒略历方法计算从开始日期(通常是在 18 或 19 世纪)起经过的天数。此方法假设需要儒略日/年值。
清单 7-9 。 实现为 julian()函数(udf_example.cc)
/* BEGIN CAB MODIFICATION */
/* Reason for Modification: */
/* This section the Julian function */
longlong julian(UDF_INIT *initid, UDF_ARGS *args,
char *is_null, char *error)
{
longlong jdate = 0;
static int DAYS_IN_MONTH[] = {31, 28, 31, 30, 31, 30, 31,
31, 30, 31, 30, 31};
longlong month = 0;
longlong day = 0;
longlong year = 0;
int i;
/* copy memory from the arguments */
month = *(longlong *)args->args[0];
day = *(longlong *) args->args[1];
year = *(longlong *) args->args[2];
/* add the days in the month for each prior month */
for (i = 0; i < month - 1; i++)
jdate += DAYS_IN_MONTH[i];
/* add the day of this month */
jdate += day;
/* find the year */
if (((year % 100) != 0) && ((year % 4) == 0))
jdate++; /*leap year!*/
/* shift day of year to left */
jdate *= 10000;
/* add the year */
jdate += year;
return jdate;
}
/* END CAB MODIFICATION */
注意变量声明后的前几行。这是一个如何将来自args数组的值封送到你自己的局部变量的例子。在本例中,我将前三个参数复制为整数值。源代码的其余部分是返回给调用者的儒略日值的计算。
注闰年的计算故意显得幼稚。我给你留了一个更正确的计算方法作为练习。提示:jdate 变量应该在什么时候递增?
如果使用的是 Windows,还需要修改udf_example.def文件,添加JULIAN函数的方法。清单 7-10 显示了更新后的udf_example.def文件。
清单 7-10 。??【UDF _ example . def】源代码
LIBRARY MYUDF
DESCRIPTION 'MySQL Sample for UDF'
VERSION 1.0
EXPORTS
metaphon_init
metaphon_deinit
metaphon
myfunc_double_init
myfunc_double
myfunc_int
myfunc_int_init
sequence_init
sequence_deinit
sequence
avgcost_init
avgcost_deinit
avgcost_reset
avgcost_add
avgcost_clear
avgcost
julian_init
julian_deinit
julian
现在你可以编译这个库了。一旦库被编译,将库复制到 MySQL 服务器安装的插件目录中。如果你运行的是 Linux,你将会复制文件udf_example.so;如果你运行的是 Windows,你将从/udf_example/debug目录中复制文件udf_example.dll。
我建议在复制文件之前停止服务器,并在复制完成后重新启动它。这是因为目标文件可能与先前的编译不同(取决于您将新函数放在哪里)。每当您对可执行代码进行更改时,遵循这一点总是一个好的做法。
继续复制库并安装函数,然后输入CREATE FUNCTION命令并尝试新函数。清单 7-11 显示了在 Windows 上安装和运行JULIAN函数的例子。
清单 7-11 。 示例执行 julian()函数
mysql> CREATE FUNCTION julian RETURNS INTEGER SONAME "udf_example.dll";
Query OK, 0 rows affected (0.00 sec)
mysql> SELECT JULIAN(8, 13, 2012);
+−−-------------------+
| JULIAN(8, 13, 2012) |
+−−-------------------+
| 2262012 |
+−−-------------------+
1 row in set (0.00 sec)
如果我想创建自己的库怎么办?
你可以使用udf_example库作为你自己库的开始,或者复制它并创建你的 UDF 库。要在/sql文件夹中编译您自己的 UDF 库,编辑/sql/CMakeLists.txt文件并复制下面的代码块,替换库名的udf_example。您需要在执行make命令之前重新运行cmake命令。
IF(WIN32 OR HAVE_DLOPEN AND NOT DISABLE_SHARED)
ADD_LIBRARY(udf_example MODULE udf_example.cc)
SET_TARGET_PROPERTIES(udf_example PROPERTIES PREFIX "")
# udf_example depends on strings
IF(WIN32)
IF(MSVC)
SET_TARGET_PROPERTIES(udf_example PROPERTIES LINK_FLAGS "/DEF:${CMAKE_CURRENT_SOURCE_DIR}/udf_example.def")
ENDIF()
TARGET_LINK_LIBRARIES(udf_example strings)
ELSE()
# udf_example is using safemutex exported by mysqld
TARGET_LINK_LIBRARIES(udf_example mysqld)
ENDIF()
ENDIF()
UDF 库可以帮助您扩展服务器的能力,以满足几乎任何计算需求。这些库很容易创建,并且只需要少量的函数来实现。除了需要 Linux 的动态加载版本之外,UDF 工作得很好,没有什么特殊的配置需求。
添加本地函数
本地函数是作为 MySQL 服务器的一部分编译的函数。它们无需从库中加载就可以使用,因此它们总是可用的。它们还可以直接访问服务器内部,这是 UDF 所不具备的,从而允许本地函数响应或启动系统操作。从ABS()到UCASE(),等等,有一长串可用的本地函数。有关当前支持的本机函数集的更多信息,请参考在线 MySQL 参考手册。
如果您想要使用的函数不可用(它不是内置的本地函数之一),您可以通过修改服务器源代码来添加自己的本地函数。现在您有了一个JULIAN函数,如果有一个等价的函数将儒略历日期转换回公历日期不是更好吗?在这一节中,我将向您展示如何添加一个新的本机函数。
添加新的本地函数的过程包括更改mysqld源代码文件。我们需要创建两个类:Item_func_gregorian 和 Create_func_gregorian。服务器为每个调用该函数的 SQL 语句实例化一次 Item _ func _ gregorian 然后它调用这个类的成员函数进行实际的计算,对结果集的每一行进行一次。Create_func_gregorian 仅在服务器启动时实例化一次。这个类只包含一个工厂成员函数,当服务器需要创建一个 Item_func_gregorian 的对象时调用这个函数..您需要更改的文件总结在表 7-1 中。
表 7-1 。对 mysqld 源代码文件的更改,用于添加新的本机函数
| 文件 | 变更描述 |
|---|---|
| item_create.cc | 添加用于注册函数、帮助器方法和符号定义的函数类定义。 |
| 物料 _str_func.h | 添加函数类定义。 |
| item_str_func.cc | 添加 Gregorian 函数的实现。 |
注意文件位于源代码树根下的
/sql目录中。
LEX 1 文件怎么了?
熟悉 MySQL 5 . 6 . 5 之前的早期版本的读者可能还记得词法分析器文件 lex*和 sql_parse.yy。这些文件仍然在源代码文件中,但是 MySQL 开发者已经通过几乎完全消除修改 lex 和 yacc 代码的需要,使添加新的函数和命令变得更加容易。正如我们将在下一节中看到的,对于 SQL 命令,我们仍然必须这样做,但是对于函数和类似的扩展,代码进行了更改,以便更容易修改和删除创建新保留字的限制。新的保留字可以对想要在 SQL 语句中使用保留字的用户施加限制。
让我们开始添加公历函数注册码。打开item_create.cc文件,添加实例化,如清单 7-12 所示。您可以添加这个行号为 2000 的行,就在其他Create_func_*类定义的后面。
清单 7-12 。?? 添加 Create_func_gregorian 类
/* BEGIN CAB MODIFICATION */
/* Reason for Modification: */
/* Add gregorian class definition */
class Create_func_gregorian : public Create_func_arg1
{
public:
virtual Item *create(THD *thd, Item *arg1);
static Create_func_gregorian s_singleton;
protected:
Create_func_gregorian() {}
virtual ∼Create_func_gregorian() {}
};
/* END CAB MODIFICATION */
来自清单 7-12 的代码创建了一个类,解析器可以用它将格里高利函数(稍后定义)与GREGORIAN符号关联起来(参见清单 7-14 )。这里的 Create 函数创建了一个Create_func_gregorian类的 singleton(所有线程都使用的类的单个实例),解析器可以用它来执行 Gregorian 函数。
接下来,我们为Create_function_gregorian方法本身添加代码。清单 7-13 显示了对这段代码的修改。您可以将这段代码放在文件中另一个Create_func_方法之后的第 4700 行。此代码用于返回 singleton 的实例,并执行 Gregorian 函数并返回其结果。这里是调用 Gregorian 函数并将结果返回给用户的地方。
清单 7-13 。?? 添加 Create_func_gregorian 方法
/* BEGIN CAB MODIFICATION */
/* Reason for Modification: */
/* Add gregorian singleton create method */
Create_func_gregorian Create_func_gregorian::s_singleton;
Item*
Create_func_gregorian::create(THD *thd, Item *arg1)
{
return new (thd->mem_root) Item_func_gregorian(arg1);
}
/* END CAB MODIFICATION */
最后,我们必须添加格里高利符号。清单 7-14 显示了定义符号所需的代码。您必须将它放在定义以下数组的部分中。
static Native_func_registry func_array[] = {
我将代码放在了GREATEST符号定义之后,因为该数组旨在按字母顺序定义符号。
清单 7-14 。 添加公历符号
/* BEGIN CAB MODIFICATION */
/* Reason for Modification: */
/* Add gregorian symbol */
{ { C_STRING_WITH_LEN("GREGORIAN") }, BUILDER(Create_func_gregorian)},
/* END CAB MODIFICATION */
看一下符号定义。注意清单 7-14 中的是如何用Create_func_gregorian类调用宏BUILDER的。调用宏是解析器将我们的公历代码与GREGORIAN符号关联起来的方式。您可能想知道当检测到符号时,如何使用这种关联来告诉解析器做什么。使用的机制被称为词法哈希。
词汇散列是 Knuth 著作中的高级散列查找过程的实现。 2 它是使用实现算法的命令行实用程序生成的。实用程序gen_lex_hash有一个名为gen_lex_hash.cc的源代码文件。这个程序生成一个文件,你可以用它来替换现有的词法哈希头文件(lex_hash.h)。我把对BUILDER宏的探索留给你进一步研究。
既然 create 函数已经实现,您需要创建一个新的类来实现该函数的代码。这是大多数开发者非常困惑的地方。Oracle 提供了大量的Item_xxx_func基类(和派生类)供您使用。例如,对于返回字符串的函数,从Item_str_func派生您的类;对于返回整数的函数,从Item_int_func派生您的类。类似地,对于返回其他类型的函数,也有其他类。这背离了可动态加载的 UDF 接口,也是你选择创建本地函数而不是可动态加载的函数的主要原因。关于有哪些Item_xxx_func类的更多信息,请参见源代码树根下的/sql目录中的item_func.h文件。
由于 Gregorian 函数会返回一个字符串,所以需要从Item_str_func类派生,在item_strfunc.h中定义类,在item_strfunc.cc中实现类。打开item_strfunc.h文件,将类定义添加到头文件中,如清单 7-15 所示。
清单 7-15 。?? 修改 item_strfunc.h 文件
/* BEGIN CAB MODIFICATION */
/* Reason for Modification: */
/* Add gregorian Item function code */
class Item_func_gregorian :public Item_str_func
{
String tmp_value;
public:
Item_func_gregorian(Item *a) :Item_str_func(a) {}
const char *func_name() const { return "gregorian"; }
String *val_str(String *);
void fix_length_and_dec()
{
max_length=30;
}
};
/* END CAB MODIFICATION */
注意在清单 7-15 中的类只有四个必须声明的函数。所需的最小函数是构造函数(Item_func_gregorian)、包含执行转换的代码的函数(val_str)、返回名称的函数(func_name)以及设置字符串参数最大长度的函数(fix_length_and_dec)。您可以添加您可能需要的任何其他组件,但是这四个组件是返回字符串的函数所必需的。
其他的项目基类(和派生类)可能需要额外的函数,比如val_int()、val_double()等等。检查您需要从中派生的类的定义,以便标识必须重写的方法;这些被称为虚函数。
还要注意,我们在清单 7-15 的中实现了一个fix_length_and_dec()方法,服务器用它来设置最大长度。在这种情况下,我们选择 30,这在很大程度上是任意的,但足够大,不会对我们返回的值造成问题。
让我们添加类实现。打开item_strfunc.cc文件,添加如清单 7-16 所示的 Gregorian 类函数的实现。您需要实现主函数val_str(),它完成儒略历到公历的运算。在另一个val_str()实现之后,您可以将它放在文件的第 4030 行。
清单 7-16 。?? 修改 item_strfunc.cc 文件
/* BEGIN CAB MODIFICATION */
/* Reason for Modification: */
/* Add gregorian function code */
String *Item_func_gregorian::val_str(String *str)
{
static int DAYS_IN_MONTH[] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
longlong jdate = args[0]->val_int();
int year = 0;
int month = 0;
int day = 0;
int i;
char cstr[30];
cstr[0] = 0;
str->length(0);
/* get date from value (right 4 digits */
year = jdate - ((jdate / 10000) * 10000);
/* get value for day of year and find current month*/
day = (jdate - year) / 10000;
for (i = 0; i < 12; i++)
if (DAYS_IN_MONTH[i] < day)
day = day - DAYS_IN_MONTH[i]; /* remainder is day of current month */
else
{
month = i + 1;
break;
}
/* format date string */
sprintf(cstr, "%d", month);
str->append(cstr);
str->append("/");
sprintf(cstr, "%d", day);
str->append(cstr);
str->append("/");
sprintf(cstr, "%d", year);
str->append(cstr);
if (null_value)
return 0;
return str;
}
/* END CAB MODIFICATION */
编译和测试新的本地函数
重新编译你的服务器并重启它。如果您在编译期间遇到错误,请返回并检查您输入的语句是否有错误。一旦错误被纠正,您就有了一个新的可执行文件,停止您的服务器,将新的可执行文件复制到您的 MySQL 安装位置,然后重新启动服务器。现在可以执行本地函数 Gregorian,如清单 7-17 和清单 7-18 所示。为了测试 Gregorian 函数的正确性,首先运行julian()命令,并将该值作为gregorian()函数的输入。
清单 7-17 。?? 运行 julian()函数
mysql> select julian(8,15,2012);
+−−-----------------+
| julian(8,15,2012) |
+−−-----------------+
| 2272012 |
+−−-----------------+
1 row in set (0.00 sec)
清单 7-18 。 运行公历()函数
mysql> select gregorian(2272012);
+−−------------------+
| gregorian(2272012) |
+−−------------------+
| 8/15/2012 |
+−−------------------+
1 row in set (0.00 sec)
添加原生函数就这些了。现在您已经了解了如何创建本地函数,您可以进一步规划与 MySQL 的集成,以包括对服务器源代码的定制。
作为一个练习,考虑添加一个新函数来计算给定日期和时间之前的年数、月数、周数、天数和小时数。这个函数可以用来告诉你需要等待事件发生多长时间。从很多方面来说,这个功能是一种倒计时,比如到你下一个生日、周年纪念或者退休的倒计时。
添加 SQL 命令
如果本地 SQL 命令不能满足您的需求,并且您无法使用用户定义的函数解决问题,则可能需要向服务器添加新的 SQL 命令。本节将向您展示如何向服务器添加您自己的 SQL 命令。
许多人认为添加新的 SQL 命令是对 MySQL 服务器源代码最困难的扩展。正如您将看到的,这个过程并不复杂,也不乏味。要添加新的 SQL 命令,您必须修改解析器(在sql/ql_yacc.yy中)并将命令添加到 SQL 命令处理代码(在sql/sql_parse.cc中),有时称为“大开关”
当客户机发出查询时,会创建一个新线程,并将 SQL 语句转发给解析器进行语法验证(或因错误而拒绝)。MySQL 解析器是使用大型的 Lex-YACC 脚本实现的,该脚本是用 Bison 编译的,使用名为 gen_lex_hash 的 MySQL 实用程序将符号转换成 hash,以便在 C 代码中使用。解析器构建一个查询结构,用于将内存中的查询语句(SQL)表示为可用于执行查询的数据结构。因此,要向解析器添加新命令,您需要一份 GNU Bison。你可以从 GNU 网站 3 下载 Bison 并安装。
什么是莱克斯和 YACC,谁是拜森?
lex 代表“词法分析器生成器”,被用作解析器来识别语言的标记、文字和语法。 YACC 代表“又一个编译器编译器”,用于识别和处理语言的语义定义。这些工具与 Bison(一个 YACC 兼容的解析器生成器,它从 Lex/YACC 代码生成 C 源代码)一起使用,提供了一个丰富的机制来创建可以解析和处理语言命令的子系统。事实上,这正是 MySQL 使用这些技术的方式。
假设您想要向服务器添加一个命令,以显示服务器中所有数据库的当前磁盘使用情况。虽然外部工具可以检索这些信息 4 ,但是您希望有一个 SQL 等价函数,可以在您自己的数据库驱动的应用中轻松使用。我们还假设您想将它添加为一个SHOW命令。具体来说,您希望能够执行命令SHOW DISK_USAGE并检索一个结果集,该结果集将每个数据库列为一行,并以千字节为单位列出所有文件(表)的总大小。
添加新的 SQL 命令包括向词法分析器添加符号,向 YACC 分析器(sql_yacc.yy)添加SHOW DISK_USAGE命令语法。Bison 必须将新的解析器编译成 C 程序,然后使用前面描述的gen_lex_hash实用程序创建新的词法散列。解析器将控制指向新命令的代码放在sql_parse.cc的大 case 语句中,并带有新命令符号的 case。
让我们从向词法分析器添加符号开始。打开lex.h文件,找到static SYMBOL symbols[]数组。你可以把这个符号变成你想要的任何东西,但是它应该是有意义的东西(像所有好的变量名一样)。请确保选择一个尚未使用的符号。在这种情况下,使用符号DISK_USAGE。这对解析器来说就像一个标签,将它标识为一个标记。在数组中放置一条语句,指示词法分析器生成符号,并将其命名为DISK_USAGE_SYM。这个列表是按照字母顺序排列的,所以把它放在合适的位置。清单 7-19 显示了添加了符号的数组的摘录。
清单 7-19 。 对 SHOW DISK_USAGE 命令的 lex.h 文件的更新
static SYMBOL symbols[] = {
{ "&&", SYM(AND_AND_SYM)},
...
{ "DISK", SYM(DISK_SYM)},
/* BEGIN CAB MODIFICATION */
/* Reason for Modification: */
/* This section identifies the tokens for the SHOW DISK_USAGE command*/
{ "DISK_USAGE", SYM(DISK_USAGE_SYM)},
/* END CAB MODIFICATION */
{ "DISTINCT", SYM(DISTINCT)},
...
接下来您需要做的是添加一个助记符来标识该命令。该助记符将在解析器中用于分配给内部查询结构,并通过sql_parse.cc文件中大型 switch 语句中的 case 来控制执行流。打开sql_cmd.h文件,将新命令添加到enum_sql_command枚举中。清单 7-20 显示了新命令助记符的修改。
清单 7-20 。??【SHOW DISK _ USAGE 命令对 sql_cmd.h 文件的修改
enum enum_sql_command {
...
SQLCOM_SHOW_SLAVE_HOSTS, SQLCOM_DELETE_MULTI, SQLCOM_UPDATE_MULTI,
SQLCOM_SHOW_BINLOG_EVENTS, SQLCOM_DO,
SQLCOM_SHOW_WARNS, SQLCOM_EMPTY_QUERY, SQLCOM_SHOW_ERRORS,
SQLCOM_SHOW_STORAGE_ENGINES, SQLCOM_SHOW_PRIVILEGES,
/* BEGIN CAB MODIFICATION */
/* Reason for Modification: */
/* Add SQLCOM_SHOW_DISK_USAGE reference */
SQLCOM_SHOW_STORAGE_ENGINES, SQLCOM_SHOW_PRIVILEGES, SQLCOM_SHOW_DISK_USAGE,
/* END CAB MODIFICATION */
SQLCOM_HELP, SQLCOM_CREATE_USER, SQLCOM_DROP_USER, SQLCOM_RENAME_USER,
SQLCOM_REVOKE_ALL, SQLCOM_CHECKSUM,
SQLCOM_CREATE_PROCEDURE, SQLCOM_CREATE_SPFUNCTION, SQLCOM_CALL,
...
现在您已经有了新的符号和命令助记符,向sql_yacc.yy文件添加代码来定义您在lex.h文件中使用的新令牌,并添加新的SHOW DISK_USAGE SQL 命令的源代码。打开sql_yacc.yy文件,将新令牌添加到令牌列表中(靠近顶部)。这些都是按字母顺序定义的(粗略地),所以要按正确的顺序放置新的令牌。清单 7-21 显示了对sql_yacc.yy文件的修改。
**清单 7-21 。向 sql_yacc.yy 文件添加令牌
...
%token DISK_SYM
/* BEGIN CAB MODIFICATION */
/* Reason for Modification: */
/* Add DISK_USAGE symbol */
%token DISK_USAGE_SYM
/* END CAB MODIFICATION */
%token DISTINCT /* SQL-2003-R */
%token DIV_SYM
%token DOUBLE_SYM /* SQL-2003-R */
...
您还需要将命令语法添加到解析器 YACC 代码中(也在sql_yacc.yy中)。找到show:标签并添加命令,如清单 7-22 所示。
清单 7-22 。 解析器语法源代码为 SHOW DISK_USAGE 命令
/* Show things */
show:
/* BEGIN CAB MODIFICATION */
/* Reason for Modification: */
/* Add show disk usage symbol parsing */
SHOW DISK_USAGE_SYM
{
LEX *lex=Lex;
lex->sql_command= SQLCOM_SHOW_DISK_USAGE;
}
|
/* END CAB MODIFICATION */
SHOW
{
小心别忘了原来
SHOW语句前的|。
您可能想知道这段代码是做什么的。这看起来相当不错,但重要的是要把这一部分做好。事实上,这是大多数开发者放弃和添加新命令失败的阶段。
每当解析器识别出SHOW标记时,就会执行由show:标签识别的代码集。YACC 代码几乎总是这样写的。5SHOW DISK_USAGE_SYM语句表示出现了SHOW和DISK_USAGE标记的唯一有效语法(按此顺序)。如果您浏览代码,您会发现其他类似的语法安排。语法语句后面的代码块获得一个指向lex结构的指针,并将command属性设置为新的命令标记SQLCOM_SHOW_DISK_USAGE。这段代码将SHOW和DISK_USAGE_SYM符号与SQLCOM_SHOW_DISK_USAGE命令匹配,以便sql_parse.cc文件中的 SQL 命令开关能够正确地将执行路由到SHOW DISK_USAGE命令的实现。
还要注意,我将这段代码放在了show:定义的开头,并在前面的SHOW语法语句前面使用了竖线符号(|)。竖线用作语法开关的“或”。因此,当且仅当语句满足语法语句定义之一时,该语句才有效。请随意查看这个文件,感受一下代码是如何工作的。不要为学习每一个细节而焦虑。我向您展示的是创建一个新命令所需的最基本的知识。如果您决定实现更复杂的命令,请研究类似命令的示例,看看它们是如何处理令牌和变量的。
接下来,将源代码添加到sql_parse.cc中的大型命令语句开关中。打开文件并向 switch 语句添加一个新的 case,如清单 7-23 所示。
清单 7-23 。?? 为新命令添加一个案例
...
case SQLCOM_SHOW_AUTHORS:
res= mysqld_show_authors(thd);
break;
/* BEGIN CAB MODIFICATION */
/* Reason for Modification: */
/* Add SQLCOM_SHOW_DISK_USAGE case statement */
case SQLCOM_SHOW_DISK_USAGE:
res = show_disk_usage_command(thd);
break;
/* END CAB MODIFICATION */
case SQLCOM_SHOW_CONTRIBUTORS:
res= mysqld_show_contributors(thd);
break;
...
注意,我刚刚添加了一个对名为show_disk_usage_command()的新函数的调用。您将把这个函数添加到sql_show.cc文件中。该函数的名称与lex.h文件中的标记、sql_yacc.yy文件中的符号以及sql_parse.cc文件中的命令开关相匹配。这不仅清楚地表明了正在发生什么,还有助于将已经很大的 switch 语句限制在一定范围内。请随意查看这个文件,因为它是执行命令语句流的核心。您应该能够找到所有的命令,比如SELECT、CREATE等等。
现在,让我们添加执行命令的代码。打开sql_show.h文件,添加新命令的函数声明,如清单 7-24 所示。我将函数声明放在了与在sql_parse.cc文件中定义的相同的函数附近。这不是必需的,但它有助于组织代码。
*清单 7-24 。*的功能声明为新命令
...
int mysqld_show_variables(THD *thd,const char *wild);
bool mysqld_show_storage_engines(THD *thd);
bool mysqld_show_authors(THD *thd);
/* BEGIN CAB MODIFICATION */
/* Reason for Modification: */
/* Add show disk usage method reference */
bool show_disk_usage_command(THD *thd);
/* END CAB MODIFICATION */
bool mysqld_show_contributors(THD *thd);
bool mysqld_show_privileges(THD *thd);
...
最后的修改是添加了show_disk_usage_command()函数的实现(清单 7-25 )。打开sql_show.cc文件,添加新命令的函数实现。清单 7-25 中的代码被删除。这是为了确保在我添加任何代码之前,新命令能够工作。如果您必须实现复杂的代码,这是一个很好的实践。只实现基本功能有助于确定您的代码更改正在工作,并且遇到的任何错误都与存根代码无关。每当修改或添加新的 SQL 命令时,遵循这一实践尤其重要。
清单 7-25 。??【show _ disk _ usage _ command】实现
/* BEGIN CAB MODIFICATION */
/* Reason for Modification: */
/* Add show disk usage method */
bool show_disk_usage_command(THD *thd)
{
List<Item> field_list;
Protocol *protocol= thd->protocol;
DBUG_ENTER("show_disk_usage");
/* send fields */
field_list.push_back(new Item_empty_string("Database",50));
field_list.push_back(new Item_empty_string("Size_in_bytes",30));
if (protocol->send_result_set_metadata(&field_list,
Protocol::SEND_NUM_ROWS |
Protocol::SEND_EOF))
DBUG_RETURN(TRUE);
/* send test data */
protocol->prepare_for_resend();
protocol->store("test_row", system_charset_info);
protocol->store("1024", system_charset_info);
if (protocol->write())
DBUG_RETURN(TRUE);
my_eof(thd);
DBUG_RETURN(FALSE);
}
/* END CAB MODIFICATION */
我想提醒大家注意一下源代码。如果您还记得,在前面的章节中,我提到过一些底层的网络函数,它们允许您构建一个结果集并将其返回给客户端。查看由/* send fields */注释指示的代码行。这段代码为结果集创建字段。在本例中,我创建了两个字段(或列),名为Database和Size_in_bytes。当执行该命令时,它们将在 MySQL 客户端实用程序中显示为列标题。
请注意protocol->XXX语句。这是我使用Protocol类向客户端发送行的地方。我首先调用prepare_for_resend()来清空缓冲区,然后尽可能多地调用重载的store()方法来设置每个字段的值(按顺序)。最后,我调用write()方法将缓冲区写入网络。如果有任何错误,我会以 true 值退出函数(这意味着产生了错误)。结束结果集并结束与客户机通信的最后一个语句是my_eof()函数,它向客户机发送一个文件结束信号。您可以使用这些相同的类、方法和函数来发送命令的结果。
磁盘使用 SYM 的编译错误
如果您想编译服务器,您可以,但是您可能会遇到关于DISK_USAGE_SYM符号的错误。如果构建服务器时没有使用 cmake 或者跳过了 cmake 步骤,就会发生这种情况。以下内容将帮助您解决这些问题。
如果你一直在研究 MySQL 源代码,你可能已经注意到有sql_yacc.cc和sql_yacc.h文件。这些文件是由 Bison 从sql_yacc.yy文件生成的。让我们使用 Bison 来生成这些文件。打开一个命令窗口,导航到源代码根目录下的/sql目录。运行命令:
bison–y–p MySQL–d SQL _ yacc . YY
这会生成两个新文件:y.tab.c和y.tab.h。这些文件将分别取代sql_yacc.cc和sql_yacc.h文件。在复制它们之前,请备份原始文件。备份文件后,将y.tab.c复制到sql_yacc.cc,将y.tab.h复制到sql_yacc.h。
一旦sql_yacc.cc和sql_yacc.h文件正确,通过运行以下命令生成词法哈希:
gen_lex_hash > lex_hash.h
现在已经为编译服务器做好了一切准备。由于您已经修改了许多关键头文件,您可能会遇到比正常情况下更长的编译时间。如果您遇到编译错误,请在继续之前更正它们。
但是,如果使用 debug 编译代码,您可能会在mysqld.cc中遇到编译错误。如果出现这种情况,很可能是调用了一个compile_time_assert()宏。如果是这种情况,修改代码如下,以补偿com_status_vars枚举数的差异。
compile_time_assert(sizeof(com_status_vars)/
sizeof(com_status_vars[0]) - 1 == SQLCOM_END + 8–1);
一旦服务器编译完成,您就有了一个新的可执行文件,停止您的服务器,将新的可执行文件复制到您的 MySQL 安装位置,然后重新启动服务器。现在,您可以在 MySQL 客户端实用程序中执行新命令。清单 7-26 显示了一个SHOW DISK_USAGE命令的例子。
清单 7-26 。 显示磁盘使用命令的执行示例
mysql> SHOW DISK_USAGE;
+−−--------+−−-------------+
| Database | Size_in_bytes |
+−−--------+−−-------------+
| test_row | 1024 |
+−−--------+−−-------------+
1 row in set (0.00 sec)
现在一切都正常了,打开sql_show.cc文件并添加SHOW DISK_USAGE命令的实际代码,如清单 7-27 所示。
清单 7-27 。??【最终展示 _ 磁盘 _ 用法 _ 命令】源代码
/* This section adds the code to call the new SHOW DISK_USAGE command. */
/* BEGIN CAB MODIFICATION */
/* Reason for Modification: */
/* Add show disk usage method */
bool show_disk_usage_command(THD *thd)
{
List<Item> field_list;
List<LEX_STRING> dbs;
LEX_STRING *db_name;
char *path;
MY_DIR *dirp;
FILEINFO *file;
longlong fsizes = 0;
longlong lsizes = 0;
Protocol *protocol= thd->protocol;
DBUG_ENTER("show_disk_usage");
/* send the fields "Database" and "Size" */
field_list.push_back(new Item_empty_string("Database",50));
field_list.push_back(new Item_return_int("Size_in_bytes", 7,
MYSQL_TYPE_LONGLONG));
if (protocol->send_result_set_metadata(&field_list,
Protocol::SEND_NUM_ROWS |
Protocol::SEND_EOF))
DBUG_RETURN(TRUE);
/* get database directories */
find_files_result res = find_files(thd, &dbs, 0, mysql_data_home,0,1);
if (res != FIND_FILES_OK)
DBUG_RETURN(1);
List_iterator_fast<LEX_STRING> it_dbs(dbs);
path = (char *)my_malloc(PATH_MAX, MYF(MY_ZEROFILL));
dirp = my_dir(mysql_data_home, MYF(MY_WANT_STAT));
fsizes = 0;
for (int i = 0; i < (int)dirp->number_off_files; i++)
{
file = dirp->dir_entry + i;
if (strncasecmp(file->name, "ibdata", 6) == 0)
fsizes = fsizes + file->mystat->st_size;
else if (strncasecmp(file->name, "ib", 2) == 0)
lsizes = lsizes + file->mystat->st_size;
}
/* send InnoDB data to client */
protocol->prepare_for_resend();
protocol->store("InnoDB TableSpace", system_charset_info);
protocol->store((longlong)fsizes);
if (protocol->write())
DBUG_RETURN(TRUE);
protocol->prepare_for_resend();
protocol->store("InnoDB Logs", system_charset_info);
protocol->store((longlong)lsizes);
if (protocol->write())
DBUG_RETURN(TRUE);
/* now send database name and sizes of the databases */
while ((db_name = it_dbs++))
{
fsizes = 0;
strcpy(path, mysql_data_home);
strcat(path, "/");
strcat(path, db_name->str);
dirp = my_dir(path, MYF(MY_WANT_STAT));
for (int i = 0; i < (int)dirp->number_off_files; i++)
{
file = dirp->dir_entry + i;
fsizes = fsizes + file->mystat->st_size;
}
protocol->prepare_for_resend();
protocol->store(db_name->str, system_charset_info);
protocol->store((longlong)fsizes);
if (protocol->write())
DBUG_RETURN(TRUE);
}
my_eof(thd);
/* free memory */
my_free(path);
my_dirend(dirp);
DBUG_RETURN(FALSE);
}
/* END CAB MODIFICATION */
注意在 Windows 上,您可能需要在
my_malloc()调用中用MAX_PATH代替PATH_MAX,用strnicmp代替strncasecmp。
当您编译并加载服务器,然后运行该命令时,您应该会看到类似于清单 7-28 中的示例。
清单 7-28 。 新 SHOW DISK_USAGE 命令的执行示例
mysql> show disk_usage;
+−−------------------+−−-------------+
| Database | Size_in_bytes |
+−−------------------+−−-------------+
| InnoDB TableSpace | 77594624 |
| InnoDB Logs | 10485760 |
| mtr | 33423 |
| mysql | 844896 |
| performance_schema | 493595 |
| test | 8192 |
+−−------------------+−−-------------+
6 rows in set (0.00 sec)
mysql>
该列表显示了 MySQL 数据目录中服务器上每个数据库的累积大小。您可能想做的一件事是添加一行,返回所有已用磁盘空间的总和(很像一个WITH ROLLUP子句)。我将这一修改留给您,让您在尝试实现该函数时完成。
我希望这篇关于创建新 SQL 命令的短文已经帮助消除了围绕 MySQL SQL 命令处理源代码的一些困惑和困难。现在您已经有了这些信息,您可以规划您自己的 MySQL 命令扩展来满足您自己的独特需求。
添加到信息模式
本章中我想讨论的最后一个领域是向信息模式中添加信息。信息模式是内存中逻辑表的集合,包含关于服务器及其环境的状态和其他相关数据(也称为元数据)。版本 5.0.2 中引入的信息模式已经成为管理和调试 MySQL 服务器、其环境和数据库的重要工具。 6 例如,通过使用以下 SQL 命令,信息模式可以轻松显示数据库中所有表的所有列:
SELECT table_name, column_name, data_type FROM information_schema.columns
WHERE table_schema = 'test';
元数据被分组到逻辑表中,允许您对它们发出SELECT命令。创建一个INFORMATION_SCHEMA视图的最大优势之一是使用SELECT命令。具体来说,您可以使用一个WHERE子句将输出限制为匹配的行。这提供了一种获取服务器信息的独特而有用的方法。表 7-2 列出了一些逻辑表及其用途。
表 7-2 。信息模式逻辑表
| 名字 | 描述 |
|---|---|
| 概要 | 提供有关数据库的信息。 |
| 桌子 | 提供有关所有数据库中的表的信息。 |
| 列 | 提供有关所有表中的列的信息。 |
| 统计数字 | 提供了有关表索引的信息。 |
| 用户 _ 权限 | 提供了有关数据库权限的信息。它封装了 mysql.db grant 表。 |
| 表 _ 权限 | 提供了有关表权限的信息。它封装了 mysql.tables_priv grant 表。 |
| 列 _ 权限 | 提供了有关列权限的信息。它封装了 mysql.columns_priv grant 表。 |
| 校对 | 提供有关字符集排序规则的信息。 |
| 关键字 _ 列 _ 用途 | 提供有关键列的信息。 |
| 例行公事 | 提供有关过程和函数的信息(不包括用户定义的函数)。 |
| 视图 | 提供有关所有数据库中视图的信息。 |
| 扳机 | 提供有关所有数据库中触发器的信息。 |
因为 disk-usage 命令属于元数据的范畴,所以我将向您展示如何将它添加到服务器的信息模式机制中。这个过程实际上非常简单,不需要修改sql_yacc.yy代码或词法哈希。相反,您可以在为磁盘使用函数创建数据(行)的函数中为 switch 语句添加一个枚举和一个 case,定义一个结构来保存表的列,然后添加源代码来执行它。
让我们从修改新枚举的头文件开始。打开handler.h文件并找到enum_schema_tables枚举。向列表中添加一个名为SCH_DISKUSAGE的新枚举。清单 7-29 显示了添加了新枚举的枚举摘录。
清单 7-29 。?? 对 enum_schema_tables 枚举的修改
enum enum_schema_tables
{
...
SCH_COLLATION_CHARACTER_SET_APPLICABILITY,
SCH_COLUMNS,
SCH_COLUMN_PRIVILEGES,
/* BEGIN CAB MODIFICATION */
/* Reason for Modification: */
/* Add case enum for the new SHOW DISK_USAGE view. */
SCH_DISKUSAGE,
/* END CAB MODIFICATION */
SCH_ENGINES,
SCH_EVENTS,
SCH_FILES,
...
现在您需要在创建新模式表的prepare_schema_tables()函数中添加 switch 命令的案例。打开sql_parse.cc文件,添加清单 7-30 中所示的 case 语句。请注意,我只是添加了没有中断语句的案例。这允许代码落入满足所有情况的代码。这是大多数源代码中冗长的if-then-else-if语句的优雅替代。
清单 7-30 。 对 prepare_schema_table 函数的修改
int prepare_schema_table(THD *thd, LEX *lex, Table_ident *table_ident,
enum enum_schema_tables schema_table_idx)
{
...
DBUG_ENTER("prepare_schema_table");
switch (schema_table_idx) {
/* BEGIN CAB MODIFICATION */
/* Reason for Modification: */
/* Add case statement for the new SHOW DISK_USAGE view. */
case SCH_DISKUSAGE:
/* END CAB MODIFICATION */
case SCH_SCHEMATA:
#if defined(DONT_ALLOW_SHOW_COMMANDS)
my_message(ER_NOT_ALLOWED_COMMAND,
...
您可能已经注意到,我将磁盘使用模式表称为DISKUSAGE。我这样做是因为已经在解析器和词法哈希中定义了DISK_USAGE标记。如果我使用DISK_USAGE并发出命令SELECT * FROM DISK_USAGE,我会得到一个错误。这是因为解析器将DISK_USAGE标记与SHOW命令相关联,而不是与SELECT命令相关联。
现在我们到了最后一组代码更改。您需要添加一个结构,信息模式函数可以用它来创建表的字段列表。打开sql_show.cc文件并添加一个类型为ST_FIELD_INFO的新数组,如清单 7-31 所示。请注意,这些列的名称和类型与show_disk_usage_command()中的相同。
清单 7-31 。??【磁盘使用模式表】新字段信息结构
/* BEGIN CAB MODIFICATION */
/* Reason for Modification: */
/* This section adds the code to call the new SHOW DISK_USAGE command. */
ST_FIELD_INFO disk_usage_fields_info[]=
{
{"DATABASE", 40, MYSQL_TYPE_STRING, 0, 0, NULL, SKIP_OPEN_TABLE},
{"Size_in_bytes", 21 , MYSQL_TYPE_LONG, 0, 0, NULL, SKIP_OPEN_TABLE },
{0, 0, MYSQL_TYPE_STRING, 0, 0, 0, SKIP_OPEN_TABLE}
};
/* END CAB MODIFICATION */
您需要做的下一个更改是在schema_tables数组中添加一行(也在sql_show.cc中)。找到数组并添加一个类似于清单 7-32 所示的语句。这说明新表名为 DISKUSAGE,列定义由 disk_usage_fields_info 指定,Create_schema_table 将用于创建表,fill_disk_usage 将用于填充表。make_old_format告诉代码确保显示列名。最后四个参数是一个指针,指向一个对表进行一些额外处理的函数、两个索引字段和一个表示它是一个隐藏表的bool变量。在示例中,我将指向函数的指针设置为NULL (0);–1表示索引未使用,0表示表格未隐藏。
清单 7-32 。?? 对 schema_tables 数组的修改
ST_SCHEMA_TABLE schema_tables[]=
{
...
{"ENGINES", engines_fields_info, create_schema_table,
fill_schema_engines, make_old_format, 0, -1, -1, 0, 0},
/* BEGIN CAB MODIFICATION */
/* Reason for Modification: */
/* This section adds the code to call the new SHOW DISK_USAGE command. */
{"DISKUSAGE", disk_usage_fields_info, create_schema_table,
fill_disk_usage, make_old_format, 0, -1, -1, 0, 0},
/* END CAB MODIFICATION */
#ifdef HAVE_EVENT_SCHEDULER
{"EVENTS", events_fields_info, create_schema_table,
fill_schema_events, make_old_format, 0, -1, -1, 0, 0},
...
好了,我们到了最后阶段。剩下的就是实现fill_disk_usage()函数了。从schema_tables数组 7 向上滚动,插入fill_disk_usage()函数的实现,如清单 7-33 所示。
清单 7-33 。??【fill _ disk _ usage】函数实现
/* BEGIN CAB MODIFICATION */
/* Reason for Modification: */
/* Add code to fill the output for the new SHOW DISK_USAGE view. */
int fill_disk_usage(THD *thd, TABLE_LIST *tables, Item *cond)
{
TABLE *table= tables->table;
CHARSET_INFO *scs= system_charset_info;
List<Item> field_list;
List<LEX_STRING> dbs;
LEX_STRING *db_name;
char *path;
MY_DIR *dirp;
FILEINFO *file;
longlong fsizes = 0;
longlong lsizes = 0;
DBUG_ENTER("fill_disk_usage");
find_files_result res = find_files(thd, &dbs, 0, mysql_data_home,0,1);
if (res != FIND_FILES_OK)
DBUG_RETURN(1);
List_iterator_fast<LEX_STRING> it_dbs(dbs);
path = (char *)my_malloc(PATH_MAX, MYF(MY_ZEROFILL));
dirp = my_dir(mysql_data_home, MYF(MY_WANT_STAT));
fsizes = 0;
for (int i = 0; i < (int)dirp->number_off_files; i++)
{
file = dirp->dir_entry + i;
if (strncasecmp(file->name, "ibdata", 6) == 0)
fsizes = fsizes + file->mystat->st_size;
else if (strncasecmp(file->name, "ib", 2) == 0)
lsizes = lsizes + file->mystat->st_size;
}
/* send InnoDB data to client */
table->field[0]->store("InnoDB TableSpace",
strlen("InnoDB TableSpace"), scs);
table->field[1]->store((longlong)fsizes, TRUE);
if (schema_table_store_record(thd, table))
DBUG_RETURN(1);
table->field[0]->store("InnoDB Logs", strlen("InnoDB Logs"), scs);
table->field[1]->store((longlong)lsizes, TRUE);
if (schema_table_store_record(thd, table))
DBUG_RETURN(1);
/* now send database name and sizes of the databases */
while ((db_name = it_dbs++))
{
fsizes = 0;
strcpy(path, mysql_data_home);
strcat(path, "/");
strcat(path, db_name->str);
dirp = my_dir(path, MYF(MY_WANT_STAT));
for (int i = 0; i < (int)dirp->number_off_files; i++)
{
file = dirp->dir_entry + i;
fsizes = fsizes + file->mystat->st_size;
}
restore_record(table, s->default_values);
table->field[0]->store(db_name->str, db_name->length, scs);
table->field[1]->store((longlong)fsizes, TRUE);
if (schema_table_store_record(thd, table))
DBUG_RETURN(1);
}
/* free memory */
my_free(path);
DBUG_RETURN(0);
}
/* END CAB MODIFICATION */
注意在 Windows 上,用
MAX_PATH代替my_malloc()调用中的PATH_MAX,用strnicmp代替strncasecmp。
我复制了前面的DISK_USAGE命令的代码,删除了创建字段的调用(通过disk_usage_fields_info array处理)和向客户端发送行的代码。相反,我使用了一个TABLE类/结构的实例来存储fields数组中的值,从第一列的零开始。对函数schema_table_store_record()的调用将值转储到网络协议。
现在已经为编译服务器做好了一切准备。由于您已经修改了其中一个关键头文件(handler.h),您可能会遇到比正常情况下更长的编译时间,因为可能需要编译mysqld项目的一些依赖项。如果您遇到编译错误,请在继续之前更正它们。
一旦服务器编译完成,您就有了一个新的可执行文件,停止您的服务器,将新的可执行文件复制到您的 MySQL 安装位置,然后重新启动服务器。现在,您可以在 MySQL 客户端实用程序中执行新命令。清单 7-34 展示了一个使用信息模式的例子,显示所有可用的模式表,并转储新的DISKUSAGE表的内容。
**清单 7-34 。示例信息模式与新磁盘使用模式一起使用表
mysql> use INFORMATION_SCHEMA;
Database changed
mysql> SHOW TABLES LIKE 'DISK%';
+−−------------------------------------+
| Tables_in_information_schema (DISK%) |
+−−------------------------------------+
| DISKUSAGE |
+−−------------------------------------+
1 row in set (0.00 sec)
mysql> SELECT * from DISKUSAGE;
+−−------------------+−−-------------+
| DATABASE | Size_in_bytes |
+−−------------------+−−-------------+
| InnoDB TableSpace | 77594624 |
| InnoDB Logs | 10485760 |
| mtr | 33423 |
| mysql | 844896 |
| performance_schema | 493595 |
| test | 8192 |
+−−------------------+−−-------------+
6 rows in set (0.00 sec)
mysql>
既然您已经知道了如何添加到信息模式中,那么您可以添加的内容就没有限制了,这样您的数据库专业人员就可以更密切地监控和调优您的 MySQL 服务器。
摘要
在这一章中,我已经向你展示了如何通过添加你自己的新函数和命令来扩展 MySQL 服务器的功能。
您了解了如何构建一个可以在运行时加载和卸载的 UDF 库,如何向服务器源代码添加一个本机函数,以及如何向解析器和查询执行代码添加一个新的SHOW命令。您还学习了如何向信息模式添加视图。
以这种方式扩展服务器的能力使得 MySQL 非常灵活。UDF 机制是最容易编码的机制之一,它在复杂性和开发速度方面远远超过了竞争对手。该服务器是开源的,这意味着您也可以直接进入源代码,并为您的特定环境添加自己的 SQL 命令。不管您是否使用这些工具,您都应该知道您不会受到“开箱即用”功能和命令的限制。
下一章将探讨 MySQL 最受欢迎的特性之一——MySQL 复制。我将介绍复制的基础知识,并带您浏览复制源代码。接下来是复制的示例扩展,您可以使用这些扩展来了解复制的内部原理,并了解可以用来增强您自己的高可用性解决方案的扩展。
词汇分析器和 yacc 文件——不要和经常被称为 Lexx 的古怪科幻程序混淆。(注:加州大学 YACC 分校?)
2 Knuth,d . e .计算机编程的艺术。第二版。(艾迪森-韦斯利,1997)。
3 Linux/Unix 用户既可以使用他们的软件包管理器并安装它,也可以从 GNU 网站下载(www.gnu.org/software/bison)。Windows 用户可以从http://gnuwin32.sourceforge.net/packages/bison.htm下载 Win32 版本。
4 例如,MySQL Utilities 实用程序 mysqldiskusage。MySQL 工具是 MySQL 工作台的一个子项目。你可以从 dev.mysql.com 的下载 MySQL Workbench。
5 要了解更多关于 YACC 解析器以及如何编写 YACC 代码的信息,请参见http://dinosaur.compilertools.net/。
6 关于信息模式的更多信息,请参见在线 MySQL 参考手册。
7 记住,如果你不使用函数声明,你必须把函数的代码放在引用它的代码的前面。