PHP-MySQL-入门教程-九-

61 阅读56分钟

PHP MySQL 入门教程(九)

原文:Beginning PHP and MySQL

协议:CC BY-NC-SA 4.0

二十七、使用 PHP 和 MySQL

MySQL 是一个关系数据库引擎/工具,它允许开发人员使用一种叫做结构化查询语言(SQL)的东西来与数据库进行交互。SQL 可用于执行两种类型的任务。第一种类型是在数据库中创建 alter 或 drop 对象。这些对象是表、视图、过程、索引等。第二种类型的命令用于通过选择、插入、更新或删除表中的行来与数据进行交互。表格可以比作包含行和列的电子表格。每一列都有一个名称、一个数据类型、一个长度以及其他定义如何处理数据的标志。尽管 SQL 被许多不同的数据库系统使用,但它们并不都遵循相同的语法或支持相同的特性;然而,它们中的大多数都遵循名为 SQL92 的标准,并具有许多自定义特性。这方面的一个例子是 MySQL 的 filed 选项AUTO_INCREMENT。当此选项应用于表中的整数列时,数据库将在每次向表中添加行时自动为该列赋值,除非 insert 语句为该列提供了值。其他数据库使用DEFAULT UNIQUE (FrontBase)或者IDENTITY() (SQL Server)。Oracle 数据库要求创建一个序列,然后使用该序列在插入时创建一个唯一值。这些差异使得编写在不同数据库系统上运行的代码变得困难。

PHP 几乎从项目一开始就支持 MySQL,包括版本 2 的 API。事实上,在 PHP 中使用 MySQL 最终变得如此普遍,以至于几年来该扩展都是默认启用的。但也许这两个技术阵营之间紧密联系的最有力证据是 PHP 5 的最新 MySQL 扩展的发布,被称为 MySQL 改进版(通常被称为MySQL)。

那么,为什么需要新的扩展呢?原因是双重的。首先,MySQL 的快速发展阻止了依赖原始扩展的用户利用新特性,如预准备语句、高级连接选项和安全性增强。第二,虽然最初的扩展确实为程序员提供了很好的服务,但许多人认为过程接口已经过时,他们更喜欢原生的面向对象的接口,这种接口不仅可以与其他应用更紧密地集成,还可以根据需要提供扩展该接口的能力。为了解决这些缺陷,MySQL 开发人员决定是时候改进扩展了,不仅改变其内部行为以提高性能,还加入了额外的功能以方便使用只有这些较新的 MySQL 版本才有的功能。主要增强功能的详细列表如下:

  • 面向对象(Object oriented):mysqli 扩展封装在一系列类中,鼓励使用许多人认为比 PHP 的传统过程方法更方便、更有效的编程范例。然而,那些喜欢接受过程化编程范例的人并不倒霉,因为它也提供了一个传统的过程化接口(尽管本章不会涉及)。

  • 准备好的语句:准备好的语句消除了重复执行查询时的开销和不便,这在构建数据库驱动的网站时很常见。准备好的声明还提供了另一个重要的与安全相关的功能,因为它们可以防止 SQL 注入攻击。

  • 事务支持:虽然 PHP 最初的 MySQL 扩展中提供了 MySQL 的事务功能,但是 MySQL 扩展为这些功能提供了一个面向对象的接口。本章介绍了相关方法,第三十四章提供了对该主题的完整讨论。

  • 增强的调试能力:mysqli 扩展提供了许多调试查询的方法,从而提高了开发过程的效率。

  • 嵌入式服务器支持:嵌入式 MySQL 服务器库在 4.0 版本中推出,适用于对在 kiosk 或桌面程序等客户端应用中运行完整的 MySQL 服务器感兴趣的用户。mysqli 扩展提供了连接和操作这些嵌入式 MySQL 数据库的方法。

  • 主/从支持:从 MySQL 3.23.15 开始,MySQL 提供了对复制的支持,尽管在以后的版本中这个特性已经有了很大的改进。使用 mysqli 扩展,您可以确保在复制配置中将写查询定向到主服务器。

安装先决条件

从 PHP 5 开始,MySQL 支持不再与标准 PHP 发行版捆绑在一起。因此,您需要显式地配置 PHP 来利用这个扩展。在本节中,您将学习如何在 Unix 和 Windows 平台上实现这一点。

在 Linux/Unix 上启用 mysqli 扩展

在 Linux/Unix 平台上启用 mysqli 扩展是通过使用--with-mysqli标志配置 PHP 来完成的。这个标志应该指向 MySQL 4.1 和更高版本可用的mysql_config程序的位置。有了今天可用的包管理器,不再需要从源代码编译 PHP 和扩展。为了启用 mysqli 扩展,只需使用 yum install php_mysql 或 get get php _ mysql 命令。这通常会将 mysqli 安装为一个共享对象,您必须通过在 php.ini 文件中添加以下行来启用该扩展:

extension=php_mysqli.so

在 Windows 上启用 mysqli 扩展

要在 Windows 上启用 mysqli 扩展,您需要从php.ini文件中取消对以下行的注释,如果它不存在,则添加它:

extension=php_mysqli.dll

在启用任何扩展之前,确保 PHP 的extension_dir指令指向适当的目录。关于配置 PHP 的更多信息,参见第二章。

使用 MySQL 本地驱动程序

历史上,PHP 要求在与 MySQL 通信的服务器上安装一个 MySQL 客户端库,无论 MySQL 服务器是位于本地还是其他地方。PHP 5.3 通过引入一个名为 MySQL Native Driver(也称为 mysqlnd)的新的 MySQL 驱动程序消除了这种不便,它提供了许多优于其前身的优势。mysql 本地驱动程序不是一个新的 API,而是一个新的管道,现有的 API(MySQL、MySQL 和 PDO_MySQL)可以使用它来与 MySQL 服务器通信。用 C 编写,紧密集成到 PHP 的架构中,并在 PHP 许可下发布,我推荐使用 mysqlnd,除非你有很好的理由不这样做。

要将 mysqlnd 与一个现有的扩展结合使用,您需要重新编译 PHP,包括一个适当的标志。例如,要将 mysqli 扩展与 mysqlnd 驱动程序结合使用,请传递以下标志:

--with-mysqli=mysqlnd

如果您计划同时使用 PDO_MySQL 和 MySQL 扩展,那么在编译 PHP 时,没有什么可以阻止您同时指定这两个扩展:

%>./configure --with-mysqli=mysqlnd --with-pdo-mysql=mysqlnd [other options]

像往常一样,用包管理器安装 PHP 和 MySQL 会解决这个问题。大多数情况下,不需要编译 PHP 或者驱动。

管理用户权限

PHP 与 MySQL 交互的约束与任何其他接口所需的约束没有什么不同。打算与 MySQL 通信的 PHP 脚本仍然必须连接到 MySQL 服务器,并选择一个数据库进行交互。除了按照这种顺序的查询之外,所有这些操作只能由拥有足够权限的用户来执行。

当脚本启动到 MySQL 服务器的连接时,以及每次提交需要权限验证的命令时,这些权限都会被传递和验证。但是,您只需要在连接时识别执行用户;除非稍后在脚本中建立了另一个连接,否则在脚本执行的剩余时间里将假定该用户的身份。在接下来的小节中,您将学习如何连接到 MySQL 服务器并传递这些凭证。

使用示例数据

当概念伴随着一系列连贯的例子时,学习一个新的主题往往会变得更容易。因此,位于名为公司的数据库中的下表产品用于以下页面中的所有相关示例:

CREATE TABLE products (
   id INT NOT NULL AUTO_INCREMENT,
   sku VARCHAR(8) NOT NULL,
   name VARCHAR(100) NOT NULL,
   price DECIMAL(5,2) NOT NULL,
   PRIMARY KEY(id)
)

该表由以下四行填充:

+-------+----------+-----------------------+-------+
| id    | sku      | name                  | price |
+-------+----------+-----------------------+-------+
|     1 | TY232278 | AquaSmooth Toothpaste |  2.25 |
|     2 | PO988932 | HeadsFree Shampoo     |  3.99 |
|     3 | ZP457321 | Painless Aftershave   |  4.50 |
|     4 | KL334899 | WhiskerWrecker Razors |  4.17 |
+-------+----------+-----------------------+-------+

使用 mysqli 扩展

PHP 的 MySQL 扩展提供了其前身所提供的所有功能,此外还增加了一些新功能,这些功能是 MySQL 发展成为全功能数据库服务器的结果。本节将介绍所有的特性,向您展示如何使用 mysqli 扩展连接到数据库服务器、查询和检索数据,以及执行各种其他重要任务。

建立和断开连接

与 MySQL 数据库的交互由连接建立和拆除来完成,分别包括连接到服务器和选择数据库,以及关闭连接。正如 mysqli 几乎所有可用的特性一样,您可以通过使用面向对象的方法或过程方法来实现这一点,尽管在本章中只讨论了面向对象的方法。

如果您选择使用面向对象的接口与 MySQL 服务器交互,您需要首先通过它的构造函数实例化 mysqli 类:

mysqli([string host [, string username [, string pswd
                    [, string dbname [, int port, [string socket]]]]]])

实例化类是通过标准的面向对象实践完成的:

$mysqli = new mysqli('localhost', 'catalog_user', 'secret', 'corporate');

一旦建立了连接,您就可以开始与数据库交互了。如果您需要连接到另一个数据库服务器或选择另一个数据库,您可以使用connect()select_db()方法。connect()方法接受与构造函数相同的参数,所以让我们直接看一个例子:

// Instantiate the mysqli class
$mysqli = new mysqli();

// Connect to the database server and select a database
$mysqli->connect('localhost', 'catalog_user', 'secret', 'corporate');

您也可以使用$mysqli->select_db方法选择一个数据库。以下示例连接到 MySQL 数据库服务器,然后选择corporate数据库:

// Connect to the database server
$mysqli = new mysqli('localhost', 'catalog_user', 'secret');

// Select the database
$mysqli->select_db('corporate');

一旦成功选择了一个数据库,就可以对它执行数据库查询。执行查询,比如使用 mysqli 扩展选择、插入、更新和删除信息,将在后面的章节中介绍。

一旦脚本执行完毕,任何打开的数据库连接都会自动关闭,资源也会被回收。然而,在整个执行过程中,一个页面可能需要几个数据库连接,每个连接都应该适当地关闭。即使在使用单个连接的情况下,在脚本结束时关闭它也是一个好的做法。在任何情况下,close()负责关闭连接。下面是一个例子:

$mysqli = new mysqli();
$mysqli->connect('localhost', 'catalog_user', 'secret', 'corporate');

// Interact with the database…

// close the connection
$mysqli->close()

处理连接错误

当然,如果您无法连接到 MySQL 数据库,那么页面上的其他内容将不会按计划进行。因此,您应该小心监视连接错误并做出相应的反应。mysqli 扩展包括一些可以用来捕获错误消息的特性,或者你可以使用异常(如第八章所介绍的)。例如,您可以使用mysqli_connect_errno()mysqli_connect_error()方法来诊断和显示关于 MySQL 连接错误的信息。

正在检索错误信息

开发人员总是朝着被称为无 bug 代码的天堂努力。然而,在除了最琐碎的项目之外的所有项目中,这种渴望几乎总是得不到满足。因此,正确地检测错误并向用户返回有用的信息是高效软件开发的重要组成部分。本节介绍两个函数,这两个函数对于解释和交流 MySQL 错误非常有用。

检索错误代码

错误号通常用来代替自然语言消息,以简化软件国际化工作,并允许定制错误消息。errno `errno`和`connect_errno属性包含执行最后一个 MySQL 函数生成的错误代码,如果没有错误发生,则包含0。当连接函数调用出错时,使用$connect_errno`属性。其原型如下:

class mysqli {
    int $errno;
    int $connect_errno;
}

下面是一个例子:

<?php
  $mysqli = new mysqli('localhost', 'catalog_user', 'secret', 'corporate');
  printf("Mysql error number generated: %d", $mysqli->connect_errno);
?>

这将返回:

Mysql error number generated: 1045

检索错误消息

属性error `error`和`connect_error`包含最近生成的错误消息,如果没有发生错误,则为空字符串。其原型如下:

class mysqli {
    string $error;
    string $connect_error;
}

消息语言依赖于 MySQL 数据库服务器,因为目标语言在服务器启动时作为一个标志传入。以下是英语消息的示例:

Sort aborted
Too many connections
Couldn't uncompress communication packet

下面是一个例子:

<?php

    // Connect to the database server
    $mysqli = new mysqli('localhost', 'catalog_user', 'secret', 'corporate');

    if ($mysqli->connect_errno) {
        printf("Unable to connect to the database:<br /> %s",
                 $mysqli->connect_error);
        exit();
    }

?>

例如,如果提供了不正确的密码,您会看到以下消息:

Unable to connect to the database:
Access denied for user 'catalog_user'@'localhost' (using password: YES)

当然,MySQL 的预设错误消息向最终用户显示起来可能有点难看,所以您可以考虑将错误消息发送到您的电子邮件地址,而不是在这种情况下显示一条更加用户友好的消息。

小费

MySQL 的错误信息有 20 种语言,存储在MYSQL-INSTALL-DIR/share/mysql/LANGUAGE/中。

将连接信息存储在单独的文件中

本着安全编程实践的精神,定期更改密码通常是个好主意。然而,因为必须在每个需要访问给定数据库的脚本中建立到 MySQL 服务器的连接,所以连接调用可能分散在大量文件中,使得这样的更改很困难。解决这一难题的简单方法并不令人惊讶——将这些信息存储在一个单独的文件中(位于 web 根目录之外),然后根据需要将该文件包含在您的脚本中。例如,mysqli 构造函数可能存储在名为mysql.connect.php的头文件中,如下所示:

<?php
    // Connect to the database server
    $mysqli = new mysqli('localhost', 'catalog_user', 'secret', 'corporate');
?>

然后可以根据需要包含该文件,如下所示:

<?php
    require 'mysql.connect.php';
    // begin database selection and queries.
?>

保护您的连接信息

如果您是使用数据库和 PHP 的新手,那么当您了解到像 MySQL 连接参数(包括密码)这样重要的信息以纯文本的形式存储在一个文件中时,可能会感到非常不安。尽管如此,您可以采取一些步骤来确保不速之客无法获取这些重要数据:

  • 使用基于系统的用户权限来确保只有拥有 web 服务器守护进程的用户才能读取该文件。在基于 Unix 的系统上,这意味着将文件所有权更改为运行 web 进程的用户的所有权,并将连接文件权限设置为 400(只有所有者拥有读取权限)。

  • 如果您连接到远程 MySQL 服务器,请记住,除非在传输过程中采取适当的步骤对数据进行加密,否则这些信息将以明文形式传递。最好的办法是使用安全套接字层(SSL)加密。

  • 有几种脚本编码产品可以让除了拥有必要解码权限的人之外的所有人都无法读取您的代码,同时代码的执行能力不受影响。Zend Guard ( www.zend.com )和 ionCube PHP 编码器( www.ioncube.com )可能是最知名的解决方案,尽管还有其他几个产品。请记住,除非您有对源代码进行编码的特殊原因,否则您应该考虑其他保护替代方案,例如操作系统目录安全性,因为它们在大多数情况下都非常有效。此外,编码器不兼容。如果您将编码代码分发到另一台服务器,则必须在该服务器上安装相同的编码产品以确保执行。

与数据库交互

绝大多数查询都围绕着创建、检索、更新和删除任务,统称为 CRUD。本节将向您展示如何制定这些查询并将其发送到数据库以供执行。

向数据库发送查询

方法query()负责将查询发送到数据库。它的原型是这样的:

class mysqli {
    mixed query(string query [, int resultmode])
}

可选的resultmode参数用于修改该方法的行为,接受两个值:

  • MYSQLI_STORE_RESULT:将结果作为缓冲集返回,这意味着整个集合将立即可用于导航。这是默认设置。虽然这个选项的代价是增加了内存需求,但它允许您一次处理整个结果集,这在您试图分析或管理结果集时非常有用。例如,您可能希望确定特定查询返回了多少行,或者您可能希望立即跳转到集合中的特定行。

  • MYSQLI_USE_RESULT:将结果作为无缓冲集合返回,这意味着将根据需要从服务器检索集合。无缓冲的结果集提高了大型结果集的性能,但是它们不允许对结果集执行各种操作,例如立即确定查询找到了多少行,或者移动到特定的行偏移量。当您试图检索大量的行时,应该考虑使用此选项,因为它需要较少的内存并产生更快的响应时间。

检索数据

您的应用很可能会花费大部分精力来检索和格式化所请求的数据。为此,您将向数据库发送SELECT查询,然后迭代结果,将每一行输出到浏览器,以您喜欢的任何方式格式化。

以下示例从 products 表中检索 sku、name 和 price 列,并按name对结果进行排序。然后,每行结果被放入三个适当命名的变量中,并输出到浏览器。

<?php

    $mysqli = new mysqli('localhost', 'catalog_user', 'secret', 'corporate');

    // Create the query
    $query = 'SELECT sku, name, price FROM products ORDER by name';

    // Send the query to MySQL
    $result = $mysqli->query($query, MYSQLI_STORE_RESULT);

    // Iterate through the result set
    while(list($sku, $name, $price) = $result->fetch_row())
        printf("(%s) %s: \$%s <br />", $sku, $name, $price);

?>

执行此示例会产生以下浏览器输出:

(TY232278) AquaSmooth Toothpaste: $2.25
(PO988932) HeadsFree Shampoo: $3.99
(ZP457321) Painless Aftershave: $4.50
(KL334899) WhiskerWrecker Razors: $4.17

请记住,使用无缓冲集执行这个示例表面上看起来是一样的(除了resultmode将被设置为MYSQLI_USE_RESULT),但是底层行为实际上是不同的。

插入、更新和删除数据

网络最强大的特征之一是它的读写格式;您不仅可以轻松发布信息进行展示,还可以邀请访问者添加、修改甚至删除数据。在第十三章中,你学习了如何使用 HTML 表单和 PHP 来达到这个目的,但是想要的动作是如何到达数据库的呢?通常,这是使用 SQL INSERT、UPDATE 或 DELETE 查询来完成的,其完成方式与 SELECT 查询完全相同。例如,要从products表中删除 AquaSmooth 牙膏条目,请执行以下脚本:

<?php

    $mysqli = new mysqli('localhost', 'catalog_user', 'secret', 'corporate');

    // Create the query
    $query = "DELETE FROM products WHERE sku = 'TY232278'";

    // Send the query to MySQL
    $result = $mysqli->query($query, MYSQLI_STORE_RESULT);

    // Tell the user how many rows have been affected
    printf("%d rows have been deleted.", $mysqli->affected_rows);

?>

当然,如果连接用户的凭证足够充分(参见第二十六章了解更多关于 MySQL 特权系统的信息),您可以自由地执行任何查询,包括创建和修改数据库、表和索引,甚至执行 MySQL 管理任务,比如创建和分配用户特权。

回收查询内存

当您检索一个特别大的结果集时,一旦您完成了对它的处理,就有必要回收该结果集所需的内存。free()方法为您处理这项任务。它的原型是这样的:

class mysqli_result {
    void free()
}

free()方法回收结果集消耗的所有内存。请记住,一旦执行了这个方法,结果集就不再可用。下面是一个例子:

<?php

    $mysqli = new mysqli('localhost', 'catalog_user', 'secret', 'corporate');

    $query = 'SELECT sku,  name,  price FROM products ORDER by name';

    $result = $mysqli->query($query, MYSQLI_STORE_RESULT);

    // Iterate through the result set
    while(list($sku, $name, $price) = $result->fetch_row())
        printf("(%s) %s: \$%s <br />", $sku, $name, $price);

    // Recuperate the query resources
    $result->free();
    // Perhaps perform some other large query

?>

解析查询结果

一旦查询执行完毕,结果集准备就绪,就该解析检索到的行了。您可以使用几种方法来检索组成每行的字段;您选择哪一个很大程度上取决于您的偏好,因为只有引用字段的方法不同。

将结果提取到对象中

因为您可能使用 mysqli 的面向对象语法,所以以面向对象的方式管理结果集是有意义的。您可以使用fetch_object()方法来实现。其语法如下:

class mysqli_result {
   array fetch_object()
}

fetch_object()方法通常在循环中调用,每次调用都会导致返回结果集中的下一行填充一个对象。然后根据 PHP 典型的对象访问语法来访问这个对象。下面是一个例子:

<?php

$query = 'SELECT sku, name, price FROM products ORDER BY name';
$result = $mysqli->query($query);

while ($row = $result->fetch_object())
{
    printf("(%s) %s: %s <br />", $row->sku, $row->name, $row->price)";
}

?>

使用索引和关联数组检索结果

mysqli 扩展还提供了分别使用fetch_array()fetch_row()方法使用关联数组和索引数组管理结果集的能力。他们的原型如下:

class mysqli_result {
    mixed fetch_array ([int resulttype])
}
class mysqli_result {
   mixed fetch_row()
}

fetch_array()方法实际上能够以关联数组、数字索引数组或两者的形式检索结果集的每一行,所以本节只演示fetch_array()方法,而不是两种方法,因为概念是相同的。默认情况下,fetch_array()检索两个数组;您可以通过将以下值之一作为resulttype传入来修改这个默认行为:

  • MYSQLI_ASSOC:以关联数组的形式返回行,键由字段名表示,值由字段内容表示。

  • MYSQLI_NUM:以数字索引数组的形式返回行,其顺序由查询中指定的字段名称的顺序决定。如果使用星号代替特定的字段列表(发出检索所有字段的查询信号),排序将对应于表定义中的字段排序。指定该选项会导致fetch_array()以与fetch_row().相同的方式运行

  • MYSQLI_BOTH:以关联数组和数字索引数组的形式返回行。因此,可以根据索引偏移量和字段名来引用每个字段。这是默认设置。

例如,假设您只想使用关联索引检索结果集:

$query = 'SELECT sku, name FROM products ORDER BY name';
$result = $mysqli->query($query);
while ($row = $result->fetch_array(MYSQLI_ASSOC))
{
    echo "Product:  {$row[‘name’]} ({$row[‘sku’]}) <br />";
}

如果希望仅通过数字索引来检索结果集,可以对示例进行以下修改:

$query = 'SELECT sku, name, price FROM products ORDER BY name';
$result = $mysqli->query($query);
while ($row = $result->fetch_array(MYSQLI_NUM))
{
    printf("(%s) %s: %d <br />", $row[0], $row[1], $row[2]);
}

假设涉及相同的数据,前面两个示例的输出与query()简介中的示例相同。

确定选定的行和受影响的行

您通常希望能够确定 SELECT 查询返回的行数,或者受 INSERT、UPDATE 或 DELETE 查询影响的行数。本节中介绍的两种方法正好可以做到这一点。

确定返回的行数

当您想了解 SELECT 查询语句返回了多少行时,$ num_rows属性非常有用。其原型如下:

class mysqli_result {
    int $num_rows
}

例如:

$query = 'SELECT name FROM products WHERE price > 15.99';
$result = $mysqli->query($query);
printf("There are %f product(s) priced above \$15.99.", $result->num_rows);

示例输出如下:

There are 5 product(s) priced above $15.99.

请记住$ num_rows只对确定 SELECT 查询检索的行数有用。如果您想检索受插入、更新或删除查询影响的行数,可以使用下面介绍的affected_rows()

确定受影响的行数

此方法检索受插入、更新或删除查询影响的总行数。其原型如下:

class mysqli_result {
    int $affected_rows
}

下面是一个例子:

$query = "UPDATE product SET price = '39.99' WHERE price = '34.99'";
$result = $mysqli->query($query);
printf("There were %d product(s) affected.", $result->affected_rows);

示例输出如下:

There were 2 products affected.

使用准备好的语句

重复执行一个查询是很常见的,每次迭代使用不同的参数。然而,使用传统的query()方法和循环机制这样做是以开销和编码便利性为代价的,前者是因为重复解析几乎相同的查询的有效性,后者是因为每次迭代都需要使用新值重复重新配置查询。为了帮助解决重复执行查询带来的问题,MySQL 支持预准备语句,它可以以低得多的开销和更少的代码行完成上述任务。

有两种预准备语句可供选择:

  • Bound parameters:Bound-parameter 变量允许您在 MySQL 服务器上存储一个查询,只有变化的数据被重复发送到服务器并集成到查询中以供执行。例如,假设您创建了一个允许用户管理商店产品的 web 应用。为了快速启动初始过程,您可以创建一个 web 表单,接受多达 20 个产品名称、id、价格和描述。因为这些信息将使用相同的查询来插入(当然,数据除外),所以使用绑定参数准备语句是有意义的。

  • 绑定结果:通过将 PHP 变量绑定到相应的检索字段,然后在必要时使用这些变量,绑定结果变体允许您使用有时难以处理的索引或关联数组从结果集中提取值。例如,您可以将检索产品信息的 SELECT 语句中的 URL 字段绑定到名为$sku, $name, $price$description的变量。

在介绍了一些关键方法之后,我们将在稍后分析前面两种场景的工作示例。

准备要执行的语句

不管您使用的是绑定参数还是绑定结果准备语句变量,您都需要首先通过使用prepare()方法来准备要执行的语句。其原型如下:

class mysqli_stmt {
    boolean prepare(string query)
}

下面是部分示例。随着您对其他相关方法的了解越来越多,将提供更多的实际例子来充分说明这种方法的用法。

<?php
    // Create a new server connection
    $mysqli = new mysqli('localhost', 'catalog_user', 'secret', 'corporate');

    // Create the query and corresponding placeholders
    $query = "SELECT sku, name, price, description
              FROM products ORDER BY sku";
    // Create a statement object
    $stmt = $mysqli->stmt_init();

    // Prepare the statement for execution
    $stmt->prepare($query);
    .. Do something with the prepared statement

    // Recuperate the statement resources
    $stmt->close();

    // Close the connection
    $mysqli->close();

?>

随着您对其他相关方法的了解越来越多,在前面的代码中“做某事…”具体指的是什么将变得显而易见,这些方法将在下面介绍。

执行准备好的语句

语句一旦准备好,就需要执行。具体何时执行取决于您是想要处理绑定参数还是绑定结果。在绑定参数的情况下,应该在参数被绑定后执行语句(使用本节后面介绍的bind_param()方法)。在绑定结果的情况下,您应该在使用bind_result()方法绑定结果之前执行该方法,这也将在本节稍后介绍。在这两种情况下,执行语句都是通过使用execute()方法来完成的。其原型如下:

class stmt {
    boolean execute()
}

参见后面对bind_param()bind_result()的介绍,了解execute()的实际例子。

回收准备好的语句资源

一旦您使用完一个准备好的语句,它所需要的资源可以用close()方法回收。其原型如下:

class stmt {
    boolean close()
}

参见前面对prepare()的介绍,了解这种方法的应用示例。

绑定参数

当使用绑定参数预备语句变量时,需要调用bind_param()方法将变量名绑定到相应的字段。其原型如下:

class stmt {
    boolean bind_param(string types, mixed &var1 [, mixed &varN])
}

类型参数表示每个变量的数据类型(由&var1,… &varN表示),需要确保在数据发送到服务器时对其进行最有效的编码。目前,有四种类型代码可用:

  • i:所有INTEGER类型

  • d:DOUBLEFLOAT类型

  • b:BLOB类型

  • s:所有其他类型(包括字符串)

绑定参数的过程最好用一个例子来解释。回到前面提到的涉及接受 20 个 URL 的 web 表单的场景,用于将这些信息插入 MySQL 数据库的代码可能类似于清单 27-1 中的代码。

<?php
    // Create a new server connection
    $mysqli = new mysqli('localhost', 'catalog_user', 'secret', 'corporate');

    // Create the query and corresponding placeholders
    $query = "INSERT INTO products SET sku=?, name=?, price=?";

    // Create a statement object
    $stmt = $mysqli->stmt_init();

    // Prepare the statement for execution
    $stmt->prepare($query);

    // Bind the parameters
    $stmt->bind_param('ssd', $sku, $name, $price);

    // Assign the posted sku array
    $skuarray = $_POST['sku'];

    // Assign the posted name array
    $namearray = $_POST['name'];

    // Assign the posted price array
    $pricearray = $_POST['price'];

    // Initialize the counter
    $x = 0;

    // Cycle through the array, and iteratively execute the query
    while ($x < sizeof($skuarray)) {
        $sku = $skuarray[$x];
        $name = $namearray[$x];
        $price = $pricearray[$x];
        $stmt->execute();
    }

    // Recuperate the statement resources
    $stmt->close();

    // Close the connection
    $mysqli->close();

?>

Listing 27-1Binding Parameters with the mysqli Extension

除了查询本身之外,本例中的所有内容都应该非常简单。注意,问号被用作数据的占位符,即 sku、名称和价格。接下来调用bind_param()方法,将变量$sky, $name, and $price绑定到由问号表示的字段占位符,顺序与它们在方法中出现的顺序相同。这个查询被准备好并发送给服务器,此时每一行数据都准备好并发送给服务器使用execute()方法进行处理。与使用字符串连接自己构建字符串相比,绑定参数是向查询字符串注入值的更安全的方式。在删除 HTML 和脚本内容之前,您可能仍然需要清理 sanitize string 变量,但是您不必担心由于来自客户端的恶意内容而导致 SQL 语句格式错误。最后,一旦处理完所有的语句,就会调用close()方法来回收资源。

小费

如果表单值数组被传入脚本的过程不明显,参见第十三章了解解释。

绑定变量

准备并执行查询后,可以使用bind_result()方法将变量绑定到检索到的字段。其原型如下:

class mysqli_stmt {
    boolean bind_result(mixed &var1 [, mixed &varN])
}

例如,假设您想要返回在products表中找到的前 30 种产品的列表。清单 27-2 中的代码将变量$sku, $name$price绑定到查询语句中检索到的字段。

<?php

    // Create a new server connection
    $mysqli = new mysqli('localhost', 'catalog_user', 'secret', 'corporate');

    // Create query
    $query = 'SELECT sku, name, price FROM products ORDER BY sku';

    // Create a statement object
    $stmt = $mysqli->stmt_init();

    // Prepare the statement for execution
    $stmt->prepare($query);

    // Execute the statement
    $stmt->execute();

    // Bind the result parameters
    $stmt->bind_result($sku, $name, $price);

    // Cycle through the results and output the data

    while($stmt->fetch())
        printf("%s, %s, %s <br />", $sku, $name, $price);

    // Recuperate the statement resources
    $stmt->close();

    // Close the connection
    $mysqli->close();

?>

Listing 27-2Binding Results with the mysqli Extension

执行清单 27-2 会产生类似如下的输出:

A0022JKL, pants, $18.99, Pair of blue jeans
B0007MCQ, shoes, $43.99, black dress shoes
Z4421UIM, baseball cap, $12.99, College football baseball cap

从准备好的语句中检索行

fetch()方法从准备好的语句结果中检索每一行,并将字段分配给绑定的结果。其原型如下:

class mysqli {
    boolean fetch()
}

参见清单 27-2 中fetch()的运行示例。

使用其他准备好的语句方法

其他几种方法对于处理预准备语句也很有用;它们在表 27-1 中进行了总结。有关行为和参数的解释,请参考本章前面与它们同名的部分。

表 27-1

其他有用的预准备语句方法

|

方法/属性

|

描述

| | --- | --- | | affected_rows | 包含受由stmt对象指定的最后一条语句影响的行数的属性。注意这仅与插入、修改和删除查询相关。 | | free() | 回收由stmt对象指定的语句消耗的内存。 | | num_rows | 包含由stmt对象指定的语句检索的行数的属性。 | | errno | 包含由stmt对象指定的最近执行的语句的错误代码的属性。 | | connect_errno | 包含由connection对象指定的最近执行的语句的错误代码的属性。 | | error | 包含由stmt对象指定的最近执行的语句的错误描述的属性。 | | connect_error | 属性,包含由 connection 对象指定的最近执行的语句中的错误说明。 |

执行数据库事务

三种新方法增强了 PHP 执行 MySQL 事务的能力。因为第三十四章专门介绍在 PHP 驱动的应用中实现 MySQL 数据库事务,所以在本节中没有提供这个主题的详细介绍。相反,出于参考目的,介绍了与提交和回滚事务相关的三种方法。示例在第三十四章中提供。

启用自动提交模式

autocommit()方法控制 MySQL 自动提交模式的行为。其原型如下:

class mysqli {
    boolean autocommit(boolean mode)
}

通过mode传递一个值TRUE来启用自动提交,而FALSE禁用它,在这两种情况下,成功时返回TRUE,否则返回FALSE

提交交易

commit()方法将当前事务提交给数据库,如果成功则返回TRUE,否则返回FALSE。其原型如下:

class mysqli {
    boolean commit()
}

回滚事务

rollback()方法回滚当前事务,如果成功则返回TRUE,否则返回FALSE。其原型如下:

class mysqli {
    boolean rollback()
}

摘要

mysqli 扩展不仅提供了比它的老兄弟更多的功能,而且——当与新的 mysqlnd 驱动程序结合使用时——提供了无与伦比的稳定性和性能。

在下一章中,您将了解到关于 PDO 的所有内容,这是另一个强大的数据库接口,正日益成为许多 PHP 开发人员的理想解决方案。

二十八、PDO 介绍

虽然所有主流数据库一般都遵循 SQL 标准,尽管程度不同,但程序员与数据库交互所依赖的接口可能会有很大差异(即使查询基本相同)。因此,应用几乎总是绑定到特定的数据库,迫使用户也安装和维护所需的数据库,即使该数据库不如企业中已经部署的其他解决方案。例如,假设您的组织需要一个专门在 Oracle 上运行的应用,但是您的组织在 MySQL 上实现了标准化。您是否准备好投入大量资源来获得在任务关键型环境中运行所需的 Oracle 知识,然后在应用的整个生命周期中部署和维护该数据库?

为了解决这种困境,聪明的程序员开始开发数据库抽象层,目标是将应用逻辑与用于与数据库通信的逻辑分离。通过这个通用接口传递所有与数据库相关的命令,应用就可以使用几种数据库解决方案中的一种,前提是数据库支持应用所需的特性,并且抽象层提供了与该数据库兼容的驱动程序。图 28-1 显示了这一过程。

img/314623_5_En_28_Fig1_HTML.png

图 28-1

使用数据库抽象层来分离应用和数据层

您可能听说过一些更广泛的实现:

  • JDBC:顾名思义,Java 数据库连接(JDBC)标准允许 Java 程序与任何有 JDBC 驱动程序的数据库进行交互。其中包括 FrontBase、Microsoft SQL Server、MySQL、Oracle 和 PostgreSQL。

  • ODBC :开放数据库连接(ODBC)接口是当今使用最广泛的抽象实现之一,受到包括 PHP 在内的各种应用和语言的支持。所有主流数据库都提供 ODBC 驱动程序,包括上面 JDBC 介绍中提到的那些。

  • Perl DBI:Perl 数据库接口模块是 Perl 与数据库通信的标准化手段,也是 PHP DB 包背后的灵感来源。

因为 PHP 提供了对 ODBC 的支持,所以在开发 PHP 驱动的应用时,您的数据库抽象需求似乎得到了解决,对吗?虽然这种(以及许多其他)解决方案很容易获得,但一种更好的解决方案已经开发了一段时间。PHP 5.1 正式发布了这个解决方案,它被称为 PHP 数据对象(PDO)抽象层。

另一个数据库抽象层?

随着 PDO 的成熟,它遇到了来自开发人员的抱怨,这些开发人员要么参与了替代数据库抽象层的开发,要么过于关注 PDO 的数据库抽象特性,而不是它提供的全部功能。事实上,PDO 是许多现有解决方案的理想替代品。然而,PDO 实际上不仅仅是一个数据库抽象层,它提供了:

  • 编码一致性:因为 PHP 的各种数据库扩展是由许多不同的贡献者编写的,尽管有共同的特性集,编码方法却很不一致。PDO 通过提供统一的单一接口消除了这种不一致性。不管数据库。此外,扩展被分成两个不同的部分:PDO 核心包含大多数 PHP 特定的代码,让各种驱动程序只关注数据。此外,PDO 的开发人员在以前构建和维护本机数据库扩展时利用了大量的知识和经验,充分利用了成功之处,并小心避免了失败之处。尽管还存在一些不一致,但总的来说,数据库特性被很好地抽象了。

  • 灵活性:因为 PDO 在运行时加载所需的数据库驱动程序,所以不需要每次使用不同的数据库时都重新配置和重新编译 PHP。例如,如果您的数据库突然需要从 Oracle 切换到 MySQL,只需加载PDO_MYSQL驱动程序(本章后面会详细介绍如何做)。

  • 面向对象的特性 : PDO 利用了 PHP 5 的面向对象特性,产生了一种比以前的解决方案更好的数据库交互方式。

  • 性能 : PDO 是用 C 语言编写的,编译成 PHP,在其他条件相同的情况下,它比用 PHP 编写的解决方案提供了相当大的性能提升,至少对于与在数据库服务器中执行查询无关的部分是如此。

有这样的优势,还有什么不喜欢的呢?这一章的作用是让你完全熟悉 PDO 和它所提供的无数功能。

PDO 的数据库选项

在撰写本文时,除了可以通过 DBLIB 和 ODBC 访问的任何数据库之外,PDO 还支持相当多的数据库,包括:

  • 4D :可通过PDO_4D驱动程序访问。

  • CUBRID :可通过PDO_CUBRID驱动程序访问。

  • 火鸟/ InterBase 6 :可通过PDO_FIREBIRD驱动访问。

  • IBM DB2 :可以通过PDO_IBM驱动程序访问。

  • Informix :可通过PDO_INFORMIX驱动程序访问。

  • 微软 SQL Server :可通过PDO_DBLIBPDO_SQLSRV驱动访问。

  • MySQL :可通过PDO_MYSQL驱动访问。

  • ODBC :可通过PDO_ODBC驱动程序访问。ODBC 本身并不是一个数据库,但是它使 PDO 能够与这个列表中没有的任何兼容 ODBC 的数据库结合使用。

  • 甲骨文:可通过PDO_OCI驱动访问。支持 Oracle 版本 8 到 11g。

  • PostgreSQL :可通过PDO_PGSQL驱动程序访问。

  • SQLite 3。X :可通过PDO_SQLITE驱动程序访问。

使用 PDO

PDO 与 PHP 长期支持的所有数据库扩展有着惊人的相似之处。因此,对于那些将 PHP 与数据库结合使用过的人来说,本节介绍的内容应该非常熟悉。如前所述,PDO 的构建考虑到了前面数据库扩展的最佳特性,因此您会发现它的方法有显著的相似性是有道理的。

本节首先简要介绍 PDO 安装过程,然后总结其目前支持的数据库服务器。出于本章剩余部分中示例的目的,使用了以下 MySQL 表:

CREATE TABLE products (
   id INT NOT NULL AUTO_INCREMENT,
   sku CHAR(8) NOT NULL,
   title VARCHAR(100) NOT NULL,
   PRIMARY KEY(id)
);

表格中已经填入了表格 28-1 中列出的产品。

表 28-1

示例产品数据

|

Id

|

SKU

|

标题

| | --- | --- | --- | | one | ZP457321 | 无痛须后水 | | Two | TY232278 | AquaSmooth 牙膏 | | three | PO988932 | 免提洗发水 | | four | KL334899 | 威士克勒克剃刀 |

安装 PDO

从 PHP 5.1 版本开始,默认启用 PDO;然而,MySQL PDO 驱动程序不是。尽管可以将 PDO 和所需的 PDO 驱动程序作为共享模块安装,但最简单的方法是静态构建 PDO 和驱动程序;完成后,您将不必进行任何其他与配置相关的更改。因为您可能目前只对 MySQL 的 PDO 驱动程序感兴趣,所以您需要做的就是在配置 PHP 时传递--with-pdo-mysql标志。

如果您在 Windows 平台上使用 PHP 5.1 或更新版本,您需要在php.ini文件中添加对 PDO 和驱动程序扩展的引用。例如,要启用对 MySQL 的支持,请将以下几行添加到 Windows 扩展部分:

extension=php_pdo.dll
extension=php_pdo_mysql.dll

和往常一样,不要忘记重启 Apache(或其他 web 服务器)以使php.ini更改生效。如果使用包管理器(yum 或 apt-get)安装 PHP,就不需要编译 PHP 或扩展,在许多情况下,所有需要的配置都将由包管理器处理。安装 PDO 驱动程序或任何其他软件包后,检查您的 php.ini 文件。

小费

您可以确定哪些 PDO 驱动程序可用于您的环境,方法是将phpinfo()加载到浏览器中并查看 PDO 部分标题下提供的列表,或者执行pdo_drivers()函数,如下所示:

<?php print_r(pdo_drivers()); ?>

连接到数据库服务器并选择数据库

在使用 PDO 与数据库交互之前,您需要建立一个服务器连接并选择一个数据库。这是通过 PDO 的构造函数完成的。其原型如下:

PDO PDO::__construct(string DSN [, string username [, string password
                     [, array driver_opts]]])

DSN ( *数据源名称)*参数由两项组成:所需的数据库驱动程序名称,以及任何必要的数据库连接变量,如主机名、端口和数据库名称。usernamepassword参数分别指定用于连接数据库的用户名和密码。最后,driver_opts数组指定了连接可能需要或期望的任何附加选项。本节结尾提供了可用选项的列表。

您可以自由地以多种方式调用构造函数。接下来介绍这些不同的方法。

将参数嵌入构造函数

连接到数据库最简单的方法是简单地将连接参数传递给构造函数。例如,可以像这样调用构造函数(特定于 MySQL):

$dbh = new PDO('mysql:host=localhost;dbname=chp28', 'webuser', 'secret');

将参数放在文件中

PDO 利用 PHP 的流特性,打开选项将 DSN 字符串放在本地或远程的单独文件中,并在构造函数中引用它,如下所示:

$dbh = new PDO('uri:file://usr/local/mysql.dsn');

确保该文件由负责执行 PHP 脚本的同一个用户拥有,并且拥有必要的权限。

参考 php.ini 文件

通过将 dsn 信息分配给名为 pdo.dsn.aliasname 的配置参数,也可以在php.ini文件中维护 DSN 信息,其中aliasname是随后提供给构造函数的 DSN 的别名。例如,以下示例将 DSN 别名为mysqlpdo:

[PDO]
pdo.dsn.mysqlpdo = 'mysql:dbname=chp28;host=localhost'

别名随后可以由 PDO 构造函数调用,如下所示:

$dbh = new PDO('mysqlpdo', 'webuser', 'secret');

与前面的方法不同,这种方法不允许在 DSN 中包含用户名和密码。

使用 PDO 的连接相关选项

PDO 有几个与连接相关的选项,您可以考虑通过将它们传递到 driver_opts 数组中来进行调整。这些选项列举如下:

  • PDO::ATTR_AUTOCOMMIT:这个选项决定了 PDO 是在执行每个查询时提交它,还是等待commit()方法被执行后再生效。

  • PDO::ATTR_CASE:您可以强制 PDO 将检索到的列字符大小写全部转换为大写,或者全部转换为小写,或者完全按照它们在数据库中的位置使用它们。这种控制是通过将该选项分别设置为三个值之一来实现的:PDO::CASE_UPPERPDO::CASE_LOWERPDO::CASE_NATURAL

  • PDO::ATTR_EMULATE_PREPARES:启用该选项可以让预处理语句利用 MySQL 的查询缓存。

  • PDO::ATTR_ERRMODE : PDO 支持三种报错模式:PDO::ERRMODE_EXCEPTIONPDO::ERRMODE_SILENTPDO::ERRMODE_WARNING。这些模式决定了什么情况会导致 PDO 报告错误。将该选项设置为这三个值中的一个来改变默认行为,即PDO::ERRMODE_EXCEPTION。这个特性将在后面的“处理错误”一节中详细讨论

  • PDO::ATTR_ORACLE_NULLS:当设置为TRUE时,该属性导致空字符串在检索时被转换为NULL。默认设置为FALSE

  • PDO::ATTR_PERSISTENT:该选项决定连接是否持久。默认设置为 FAL SE

  • PDO::ATTR_PREFETCH:预取是一种数据库特性,即使客户端一次请求一行,也可以检索几行,原因是如果客户端请求一行,他很可能会想要其他行。这样做可以减少数据库请求的数量,从而提高效率。此选项为支持此功能的驱动程序设置预取大小,以千字节为单位。

  • PDO::ATTR_TIMEOUT:该选项设置超时前等待的秒数。MySQL 目前不支持这个选项。

  • PDO::DEFAULT_FETCH_MODE:您可以使用这个选项来设置默认的获取模式(关联数组、索引数组或对象),如果您一直喜欢某个特定的方法,那么这样可以节省一些输入。

有四个属性可以帮助您了解有关客户端、服务器和连接状态的更多信息。可以使用“获取和设置属性”一节中介绍的方法getAttribute() ,来检索属性值。

  • PDO::ATTR_SERVER_INFO:包含特定于数据库的服务器信息。对于 MySQL,它检索与服务器正常运行时间、总查询数、每秒执行的平均查询数以及其他重要信息相关的数据。

  • PDO::ATTR_SERVER_VERSION:包含与数据库服务器版本号相关的信息。

  • PDO::ATTR_CLIENT_VERSION:包含数据库客户端版本号的相关信息。

  • PDO::ATTR_CONNECTION_STATUS:包含关于连接状态的数据库特定信息。例如,在使用 MySQL 成功连接后,属性包含“通过 TCP/IP 的本地主机”,而在 PostgreSQL 上,它包含“连接正常;等着发。”

处理连接错误

在出现连接错误的情况下,除非返回的PDOException对象被正确捕获,否则脚本会立即终止。当然,你可以使用第八章第一次介绍的异常处理语法很容易地做到这一点。以下示例说明了如何在出现连接问题时捕获异常:

<?php
    try {
       $dbh = new PDO('mysql:host=localhost;dbname=chp28', 'webuser', 'secret');
    } catch (PDOException $exception) {
       echo "Connection error: " . $exception->getMessage();
    }
?>

一旦建立了连接,就该开始使用它了。这是本章其余部分的主题。

处理错误

PDO 提供了三种错误模式,允许您调整扩展处理错误的方式:

  • PDO::ERRMODE_EXCEPTION:使用PDOException类抛出一个异常,它立即停止脚本执行并提供与问题相关的信息。

  • 发生错误时不做任何事情,让开发人员检查错误并决定如何处理。这是默认设置。

  • PDO::ERRMODE_WARNING:如果发生与 PDO 相关的错误,产生一条 PHP E_WARNING消息。

要设置错误模式,只需使用setAttribute()方法,如下所示:

$dbh->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

还有两种方法可用于检索错误信息。接下来介绍两者。

正在检索 SQL 错误代码

SQL 标准提供了一个诊断代码列表,用于表示 SQL 查询的结果,称为 SQLSTATE 代码。对 SQLSTATE 代码执行 web 搜索,以生成这些代码及其含义的列表。errorCode()方法用于返回这个标准的 SQLSTATE 代码,您可以选择存储该代码用于日志记录,甚至用于生成您自己的定制错误消息。其原型如下:

int PDOStatement::errorCode()

例如,下面的脚本试图插入一个新产品,但是错误地引用了单个版本的products表:

<?php
    try {
        $dbh = new PDO('mysql:host=localhost;dbname=chp28', 'webuser', 'secret');
    } catch (PDOException $exception) {
        printf("Connection error: %s", $exception->getMessage());
    }

    $query = "INSERT INTO product(id, sku, title)
              VALUES(NULL, 'SS873221', 'Surly Soap') ";

    $dbh->exec($query);

    echo $dbh->errorCode();
?>

这应该产生代码42S02,对应于 MySQL 不存在的表消息。当然,这个消息本身意义不大,所以您可能会对接下来介绍的errorInfo()方法感兴趣。

正在检索 SQL 错误消息

errorInfo()方法产生一个数组,其中包含与最近执行的数据库操作相关的错误信息。其原型如下:

array PDOStatement::errorInfo()

该数组由三个值组成,每个值由一个在02之间的数字索引值引用:

  • 0:存储 SQL 标准中定义的 SQLSTATE 代码,

  • 1:存储数据库驱动程序特有的错误代码,

  • 2:存储数据库驱动程序特有的错误信息,

以下脚本演示了errorInfo(),使其输出与丢失的表相关的错误信息(在这种情况下,程序员错误地使用了现有products表的单数形式):

<?php
    try {
       $dbh = new PDO('mysql:host=localhost;dbname=chp28', 'webuser', 'secret');
    } catch (PDOException $exception) {
        printf("Failed to obtain database handle %s", $exception->getMessage());
    }

    $query = "INSERT INTO product(id, sku, title)
              VALUES(NULL, 'SS873221', 'Surly Soap') ";

    $dbh->exec($query);

    print_r($dbh->errorInfo());

?>

假设 product 表不存在,将产生以下输出(为便于阅读而格式化):

Array (
[0] => 42S02
[1] => 1146
[2] => Table 'chp28.product' doesn't exist )

获取和设置属性

相当多的属性可用于调整 PDO 的行为。因为可用属性的数量相当大,而且事实上几个数据库驱动程序提供了它们自己的自定义属性,所以将您指向 www.php.net/pdo 以获取最新信息是有意义的,而不是在此详尽地列出所有可用属性。

下一节将介绍可用于设置和检索这些属性值的方法。

正在检索属性

getAttribute()方法检索由 attribute 指定的属性的值。它的原型是这样的:

mixed PDOStatement::getAttribute(int attribute)

下面是一个例子:

<?php

$dbh = new PDO('mysql:host=localhost;dbname=chp28', 'webuser', 'secret');
echo $dbh->getAttribute(PDO::ATTR_CONNECTION_STATUS);

?>

在我的服务器上,这会返回:

localhost via TCP/IP

设置属性

setAttribute()方法将由指定的值赋给由属性指定的属性。它的原型是这样的:

boolean PDOStatement::setAttribute(int attribute, mixed value)

例如,要设置 PDO 的错误模式,您需要像这样设置PDO::ATTR_ERRMODE:

$dbh->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

执行查询

PDO 提供了几种执行查询的方法,每种方法都适合以最有效的方式执行特定的查询类型。以下列表细分了每种查询类型:

  • 执行没有结果集的查询:执行插入、更新、删除等查询时,不返回结果集。在这种情况下,exec()方法返回受查询影响的行数。

  • 执行一次查询:当执行一个返回结果集的查询时,或者当受影响的行数无关紧要时,应该使用query()方法。

  • 多次执行一个查询:虽然可以使用while循环和query()方法多次执行一个查询,每次迭代传递不同的列值,但是使用准备好的语句这样做更有效。

添加、修改和删除表格数据

您的应用可能会提供一些添加、修改和删除数据的方法。为此,您可以将一个查询传递给exec()方法,该方法执行一个查询并返回受其影响的行数。其原型如下:

int PDO::exec(string query)

考虑以下示例:

<?php

$query = "UPDATE products SET title='Painful Aftershave' WHERE sku="ZP457321"";
// Be aware of SQL injections when building query strings
$affected = $dbh->exec($query);
echo "Total rows affected: $affected";

?>

根据本章前面介绍的示例数据,此示例将返回以下内容:

Total rows affected: 1

请注意,该方法不应与SELECT查询结合使用;相反,应该使用query()方法来实现这些目的。使用字符串连接来构建查询字符串,尤其是当它包含来自客户端的数据时,这不是避免 SQL 注入的安全方法。请改用准备好的语句。

选择表格数据

query()方法执行一个查询,以 PDOStatement 对象的形式返回数据。其原型如下:

PDOStatement query(string query)

下面是一个例子:

<?php

$query = 'SELECT sku, title FROM products ORDER BY id';
// Be aware of SQL injections when building query strings

foreach ($dbh->query($query) AS $row) {
    printf("Product: %s (%s) <br />", $row['title'], $row['sku']);
}

?>

基于示例数据,此示例生成以下内容:

Product: AquaSmooth Toothpaste (TY232278)
Product: HeadsFree Shampoo (PO988932)
Product: Painless Aftershave (ZP457321)
Product: WhiskerWrecker Razors (KL334899)

小费

如果您使用query()并想了解更多关于受影响的总行数,请使用rowCount()方法。

介绍准备好的声明

预准备语句是许多数据库系统的一个特征。它们至少在两个不同的方面有用。第一个也是最重要的一个是关于安全性。使用预准备语句将有助于防范 SQL 注入,在这种情况下,恶意客户端将内容发送回 web 服务器,但绕过了网页的内容检查,并提交了将执行附加任务的字符串(参见下面的示例)。当多次执行相似的语句时,可以发现预准备语句的第二个好处。在这种情况下,数据库引擎可以一次性分析语句的基本结构,并使用每次执行收集的信息,从而提高性能。批量插入或更新(一次一条记录)就是这种方法的一个很好的应用。

考虑以下代码:

<?php
$query = "select * from product where sku = '{$_POST['sku']}';";
...

首先,没有对变量$_POST['sku']进行健全性检查。它用于将内容直接传递到查询字符串中。即使 web 页面被设计为验证每个字段的内容,也不能保证客户端会根据该逻辑提交表单。请记住,在浏览器中呈现时,网页不受您的控制。如果恶意访问者将以下内容放入表单的 sku 字段,该怎么办:

'; delete from products;

该字符串将被直接添加到查询字符串中,您最终可能会删除该表中的所有行。

PDO 为支持这一特性的数据库提供了预准备语句功能。因为 MySQL 支持预处理语句,所以您可以自由地利用这个特性。准备好的语句使用两种方法来完成,一种是prepare(),它负责为执行查询做准备,另一种是execute(),它使用一组提供的列参数来重复执行查询。这些参数可以通过将它们作为数组传递给方法来显式地提供给execute(),或者通过使用使用bindParam()方法分配的绑定参数来提供。接下来将介绍这三种方法。

使用准备好的语句

prepare()方法负责为执行查询做准备。其原型如下:

PDOStatement PDO::prepare(string query [, array driver_options])

用作预准备语句的查询看起来与您可能习惯的查询略有不同,因为对于那些在执行迭代中会发生变化的列值,必须使用占位符而不是实际的列值。支持两种语法变体,命名参数问号参数。例如,使用命名参数的查询可能如下所示:

INSERT INTO products SET sku =:sku, name =:name;

使用问号参数的相同查询如下所示:

INSERT INTO products SET sku = ?, name = ?;

您所选择的变体完全是个人喜好的问题,尽管使用命名参数可能更为明确,并且您不必被迫以正确的顺序传递参数。因此,在相关示例中使用了这种变体。首先,下面的例子使用prepare()来准备一个迭代执行的查询:

// Connect to the database
$dbh = new PDO('mysql:host=localhost;dbname=chp28', 'webuser', 'secret');

$query = "INSERT INTO products SET sku =:sku, name =:name";
$stmt = $dbh->prepare($query);

一旦准备好查询,就必须执行它。这是通过下面介绍的execute()方法完成的。

除了查询之外,您还可以通过 driver_options 参数传递数据库驱动程序特定的选项。有关这些选项的更多信息,请参见 PHP 手册。

执行准备好的查询

execute()方法负责执行准备好的查询。其原型如下:

boolean PDOStatement::execute([array input_parameters])

这种方法需要输入参数,这些参数应该在每次迭代执行时被替换。这可以通过两种方式实现:要么将值作为数组传递给方法,要么使用bindParam()方法将值绑定到它们各自的变量名或查询中的位置偏移量。接下来将介绍第一个选项,第二个选项将在接下来的bindParam()介绍中介绍。

以下示例显示了一条语句是如何由execute()准备并重复执行的,每次都使用不同的参数:

<?php
    // Connect to the database server
    $dbh = new PDO('mysql:host=localhost;dbname=chp28', 'webuser', 'secret');

    // Create and prepare the query
    $query = "INSERT INTO products SET sku =:sku, title =:title";
    $stmt = $dbh->prepare($query);

    // Execute the query
    $stmt->execute( [':sku' => 'MN873213', ':title' => 'Minty Mouthwash'] );

    // Execute again
    $stmt->execute( [':sku' => 'AB223234', ':title' => 'Lovable Lipstick'] );
?>

接下来我们将再次讨论这个例子,在这里您将学习使用bindParam()方法传递查询参数的另一种方法。

绑定参数

您可能已经在前面对execute()方法的介绍中注意到,输入参数是可选的。这很方便,因为如果需要传递大量变量,以这种方式提供数组会很快变得难以处理。那么还有什么选择呢?bindParam()法。其原型如下:

boolean PDOStatement::bindParam(mixed parameter, mixed &variable [, int datatype [, int length [, mixed driver_options]]])

使用命名参数时,参数是使用语法:title在准备好的语句中指定的列值占位符的名称。使用问号参数时,参数是位于查询中的列值占位符的索引偏移量。variable 参数存储分配给占位符的值。它被描述为通过引用传递,因为当将该方法与准备好的存储过程结合使用时,该值可能会根据存储过程中的某些操作而改变。本节将不演示此功能;不过,在你读完第三十二章之后,这个过程应该就相当明显了。可选的数据类型参数显式设置参数数据类型,并且可以是以下任意值:

  • PDO::PARAM_BOOL : SQL BOOLEAN 数据类型

  • PDO::PARAM_INPUT_OUTPUT:当参数被传递到存储过程中时使用,因此可以在过程执行后更改

  • PDO::PARAM_INT : SQL INTEGER 数据类型

  • PDO::PARAM_NULL : SQL NULL 数据类型

  • PDO::PARAM_LOB : SQL 大型对象数据类型

  • PDO_PARAM_STMT : PDOStatement对象类型;目前不运行

  • PDO::PARAM_STR : SQL 字符串数据类型

可选的长度参数指定数据类型的长度。只有在赋予它PDO::PARAM_INPUT_OUTPUT数据类型时才需要它。最后, driver_options 参数用于传递任何特定于驱动程序的选项。

以下示例重温了上一个示例,这次使用bindParam()来分配列值:

<?php

    // Connect to the database server
    $dbh = new PDO('mysql:host=localhost;dbname=chp28', 'webuser', 'secret');

    // Create and prepare the query
    $query = "INSERT INTO products SET sku =:sku, title =:title";
    $stmt = $dbh->prepare($query);

    $sku = 'MN873213';
    $title = 'Minty Mouthwash';

    // Bind the parameters
    $stmt->bindParam(':sku', $sku);
    $stmt->bindParam(':title', $title);

    // Execute the query
    $stmt->execute();

    $sku = 'AB223234';
    $title = 'Lovable Lipstick';

    // Bind the parameters
    $stmt->bindParam(':sku', $sku);
    $stmt->bindParam(':title', $title);

    // Execute again
    $stmt->execute();
?>

如果使用问号参数,该语句将如下所示:

$query = "INSERT INTO products SET sku = ?, title = ?";

因此,相应的bindParam()调用如下所示:

$stmt->bindParam(1, $sku);
$stmt->bindParam(2, $title);
. . .
$stmt->bindParam(1, $sku);
$stmt->bindParam(2, $title);

检索数据

PDO 的数据检索方法与其他数据库扩展中的方法非常相似。事实上,如果你在过去使用过这些扩展,你会对 PDO 的五个相关方法感到非常舒服。本节中介绍的所有方法都是PDOStatement类的一部分,该类由前一节中介绍的几个方法返回。

返回检索到的列数

columnCount()方法返回结果集中返回的列总数。其原型如下:

integer PDOStatement::columnCount()

下面是一个例子:

// Execute the query
$query = 'SELECT sku, title FROM products ORDER BY title';
$result = $dbh->query($query);

// Report how many columns were returned
printf("There were %d product fields returned.", $result->columnCount());

示例输出如下:

There were 2 product fields returned.

检索结果集中的下一行

fetch()方法返回结果集中的下一行,或者如果已经到达结果集的末尾,则返回FALSE。它的原型是这样的:

mixed PDOStatement::fetch([int fetch_style [, int cursor_orientation
                          [, int cursor_offset]]])

引用行中每一列的方式取决于如何设置 fetch_style 参数。有八种设置可供选择:

  • PDO::FETCH_ASSOC:提示fetch()检索由列名索引的值数组。

  • PDO::FETCH_BOTH:提示fetch()获取一个数组值,该数组值由列名和行中该列的数字偏移量(从 0 开始)索引。这是默认设置。

  • PDO::FETCH_BOUND:提示fetch()返回 TRUE,并将检索到的列值分配给在bindParam()方法中指定的相应变量。有关绑定列的更多信息,请参见“设置绑定列”一节。

  • PDO::FETCH_CLASS:提示fetch()通过将结果集的列分配给同名的类属性来填充对象。

  • PDO::FETCH_INTO:将列值检索到一个类的现有实例中。各个类属性必须与列值匹配,并且必须被指定为公共范围。或者,必须重载__get()__set()方法,以便于赋值,如第七章所述。

  • PDO::FETCH_LAZY:除了包含列属性的对象之外,创建关联和索引数组,允许您使用三个接口中您选择的任何一个。

  • PDO::FETCH_NUM:提示fetch()获取一个数组值,该数组值由行中列的数字偏移量索引(从 0 开始)。

  • PDO::FETCH_OBJ:提示fetch()创建一个对象,该对象由匹配每个检索到的列名的属性组成。

如果对象是可滚动游标,则参数 cursor_orientation 确定检索哪一行,这是一个结果集,允许您在不获取所有行的情况下对行进行迭代。 cursor_offset 参数是一个整数值,表示要检索的行相对于当前光标位置的偏移量。

以下示例从数据库中检索所有产品,并按标题对结果进行排序:

<?php

    // Connect to the database server
    $dbh = new PDO("mysql:host=localhost;dbname=chp28", "webuser", "secret");

    // Execute the query
    $stmt = $dbh->query('SELECT sku, title FROM products ORDER BY title');

    while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
        printf("Product: %s (%s) <br />", $row['title'], $row['sku']);
    }

?>

示例输出如下:

Product: AquaSmooth Toothpaste (TY232278)
Product: HeadsFree Shampoo (PO988932)
Product: Painless Aftershave (ZP457321)
Product: WhiskerWrecker Razors (KL334899)

同时返回所有结果集行

fetchAll()方法的工作方式与fetch()非常相似,除了对它的一次调用会导致检索结果集中的所有行并将其分配给返回的数组。其原型如下:

array PDOStatement::fetchAll([int fetch_style])

引用检索到的列的方式取决于如何设置可选的 fetch_style 参数,默认设置为PDO_FETCH_BOTH。关于所有可用的fetch_style值的完整列表,参见前面关于fetch()方法的部分。

以下示例产生与fetch()简介中提供的示例相同的结果,但这一次取决于fetchAll()准备输出数据:

<?php

function formatRow($row) {
    return sprintf("Product: %s (%s) <br />", $row[1], $row[0]);
}

// Execute the query
$stmt = $dbh->query('SELECT sku, title FROM products ORDER BY title');

// Retrieve all of the rows
$rows = $stmt->fetchAll();

// Output the rows
echo explode(array_map('formatRow', $rows));

?>

示例输出如下:

Product: AquaSmooth Toothpaste (TY232278)
Product: HeadsFree Shampoo (PO988932)
Product: Painless Aftershave (ZP457321)
Product: WhiskerWrecker Razors (KL334899)

至于你是否选择使用fetchAll()而不是fetch(),这似乎很大程度上是一个方便的问题。但是,请记住,将fetchAll()与特别大的结果集结合使用可能会在数据库服务器资源和网络带宽方面给系统带来很大的负担。

提取单个列

fetchColumn()方法返回位于结果集下一行的单个列值。其原型如下:

string PDOStatement::fetchColumn([int column_number])

分配给column_number的列引用必须根据其在行中的数值偏移量来指定,偏移量从 0 开始。如果没有设置值,fetchColumn()返回在第一列中找到的值。奇怪的是,使用这种方法不可能在同一行中检索多列,因为每次调用都会将行指针移动到下一个位置;因此,如果需要的话,可以考虑使用fetch()

下面的例子演示了fetchColumn()并显示了对该方法的后续调用如何移动行指针:

    // Execute the query
    $result = $dbh->query('SELECT sku, title FROM products ORDER BY title');

    // Fetch the first row first column
    $sku = $result->fetchColumn(0);

    // Fetch the second row second column
    $title =  $result->fetchColumn(1);

    // Output the data.
    echo "Product: $title ($sku)";

结果输出如下。注意,产品标题和 SKU 并不对应于示例表中提供的正确值,因为如上所述,行指针随着对fetchColumn()的每次调用而前进;因此,使用这种方法时要谨慎。

Product: AquaSmooth Toothpaste (PO988932)

设置绑定列

在上一节中,您学习了如何在fetch()fetchAll()方法中设置 fetch_style 参数,以控制脚本如何使用结果集列。您可能对PDO_FETCH_BOUND设置感兴趣,因为它似乎让您在检索列值时完全避免了一个额外的步骤,只需将它们自动分配给预定义的变量。事实上,就是这样,并且是使用bindColumn()方法完成的。

bindColumn()方法用于将一个列名与一个期望的变量名相匹配,在每次检索行时,将导致相应的列值被自动分配给该变量。这使得从结果集中移动数据变得很容易,但是它不会对数据进行任何检查或格式化。这必须由代码提供。其原型如下:

boolean PDOStatement::bindColumn(mixed column, mixed &param [, int type
                                 [, int maxlen [, mixed driver_options]]])

参数指定行中的列偏移量,而 &参数定义相应变量的名称。您可以通过使用 type 参数定义变量值的类型,并使用 maxlen 参数限制其length来设置变量值的约束。支持七个type参数值。完整的列表见前面对bindParam()的介绍。

以下示例从products表中选择skutitle列,其中id等于 2,并分别根据数值偏移量和关联映射绑定结果:

<?php
    // Connect to the database server
    $dbh = new PDO('mysql:host=localhost;dbname=chp28', 'webuser', 'secret');

    // Create and prepare the query
    $query = 'SELECT sku, title FROM products WHERE id=2';
    $stmt = $dbh->prepare($query);
    $stmt->execute();

    // Bind according to column offset
    $stmt->bindColumn(1, $sku);

    // Bind according to column title
    $stmt->bindColumn('title', $title);

    // Fetch the row
    $row = $stmt->fetch(PDO::FETCH_BOUND);

    // Output the data
    printf("Product: %s (%s)", $title, $sku);
?>

它返回以下内容:

Painless Aftershave (TY232278)

使用事务

PDO 为那些能够执行事务的数据库提供事务支持。三种 PDO 方法促进事务性任务:beginTransaction() , commit() ,rollback()。因为第三十四章是关于交易的,所以这里不提供例子;相反,提供了对这三种方法的简要介绍。

开始交易

beginTransaction()方法禁用自动提交模式,这意味着在执行commit()方法之前,任何数据库更改都不会生效。其原型如下:

boolean PDO::beginTransaction()

一旦commit()rollback()被执行,自动提交模式将自动再次启用。

提交交易

commit()方法提交事务。其原型如下:

boolean PDO::commit()

回滚事务

rollback()方法否定自beginTransaction()执行以来所做的任何数据库更改。其原型如下:

boolean PDO::rollback()

摘要

PDO 为用户提供了一种强大的方法来整合不一致的数据库命令,允许用一种几乎微不足道的方法将应用从一个数据库解决方案迁移到另一个数据库解决方案。此外,如果您的客户期望一个允许他们使用首选数据库的应用,由于特定于语言和特定于数据库的特性的分离,它鼓励 PHP 语言开发人员提高生产率。

二十九、存储例程

本书中的许多例子都涉及将 MySQL 查询直接嵌入到 PHP 脚本中。事实上,对于较小的应用来说,这很好;然而,随着应用复杂性和规模的增加,您可能会想寻找更有效的方法来管理您的 SQL 代码。值得注意的是,有些查询会达到一定的复杂程度,需要您在查询中加入一定程度的逻辑,才能获得想要的结果。假设您部署了两个应用:一个面向 Web,另一个面向移动设备,这两个应用都使用相同的 MySQL 数据库并执行许多相同的任务。如果一个查询发生了变化,您需要对查询出现的地方进行修改,不是在一个应用中,而是在两个或更多的应用中!

使用复杂应用时出现的另一个挑战涉及到为每个成员提供贡献其专业知识的机会,而不一定会影响到其他人。通常,负责数据库开发和维护的个人在编写高效和安全的查询方面特别在行。但是,如果查询嵌入在代码中,这个人如何编写和维护这些查询而不干扰应用开发人员呢?此外,数据库架构师如何确信开发人员没有修改查询,从而可能在过程中打开安全漏洞?

应对这些挑战的最常见的解决方案之一是一种数据库特性,称为存储例程(通常称为存储过程)。存储例程是存储在数据库服务器中的一组 SQL 语句,通过在查询中调用指定的名称来执行,就像函数封装了一组在调用函数名称时执行的命令一样。然后,可以在数据库服务器的安全范围内维护存储的例程,而不必接触应用代码。

本章通过讨论语法和展示如何创建、管理和执行存储例程,告诉你 MySQL 是如何实现存储例程的。您还将学习如何通过 PHP 脚本将存储的例程合并到 web 应用中。首先,花点时间回顾一下关于它们优缺点的更正式的总结。

应该使用存储例程吗?

与其盲目地追随存储例程,不如花点时间考虑一下它们的优缺点,特别是因为它们的实用性是数据库社区中一个激烈争论的话题。本节总结了将存储例程合并到开发策略中的利弊。

存储的常规优势

存储例程有许多优点,其中最突出的优点如下:

  • **一致性:**当用不同语言编写的多个应用执行相同的数据库任务时,将这些相似的功能整合到存储例程中可以减少冗余的开发过程。

  • **性能:**在编写优化查询时,一个称职的数据库管理员可能是团队中知识最丰富的成员。因此,通过将这样的查询作为存储例程集中维护,为这个人保留任务可能是有意义的。

  • **安全性:**在金融、医疗和国防等特别敏感的环境中工作时,通常要求严格限制对数据的访问。使用存储例程是确保开发人员只能访问执行任务所必需的信息的好方法。

  • **架构:**虽然讨论多层架构的优点超出了本书的范围,但是将存储例程与数据层结合使用可以进一步促进大型应用的可管理性。在网上搜索 n 层架构以获得关于这个主题的更多信息。

存储的常规缺点

尽管前面的优点可能会让您相信存储例程是一种可行的方法,但是请花点时间考虑一下下面的缺点:

  • 许多人认为数据库的唯一目的是存储数据和维护数据关系,而不是执行本来可以由应用执行的代码。除了有损于许多人所认为的数据库的唯一作用之外,在数据库中执行这样的逻辑将消耗额外的处理器和内存资源。

  • **功能:**您很快就会知道,SQL 语言结构确实提供了相当多的功能和灵活性;然而,大多数开发人员发现,使用成熟的编程语言(如 PHP)构建这些例程既容易又舒服。

  • **可维护性:**虽然你可以使用基于图形用户界面的工具,比如 MySQL 查询浏览器(见第二十四章)来管理存储的例程,但是对它们进行编码和调试比使用一个有能力的 IDE 编写基于 PHP 的函数要困难得多。

  • **可移植性:**由于存储例程通常使用特定于数据库的语法,如果您需要将应用与另一个数据库产品结合使用,可能会出现可移植性问题。

因此,即使在回顾了优点和缺点之后,您可能仍然想知道存储例程是否适合您。我建议您继续阅读并尝试本章中提供的大量示例。

MySQL 如何实现存储例程

尽管术语存储例程广为流传,但 MySQL 实际上实现了两个过程变量,统称为存储例程:

  • **:存储过程:**存储过程支持执行SELECTINSERTUPDATEDELETE等 SQL 命令。他们还可以设置参数,这些参数可以在以后从过程外部引用。

  • **存储函数:**存储函数只支持执行SELECT命令,只接受输入参数,并且必须返回一个且只有一个值。此外,您可以将一个存储函数直接嵌入到一个 SQL 命令中,就像您对标准 MySQL 函数如count()date_format()所做的那样。

一般来说,当需要处理数据库中的数据时,可以使用存储过程,例如检索行或插入、更新和删除值,而使用存储函数来操作数据或执行特殊计算。事实上,对于这两种变体,本章给出的语法实际上是相同的,除了当使用存储过程时,语法将使用术语过程而不是函数。例如,命令DROP PROCEDURE procedure_name用于删除现有的存储过程,而DROP FUNCTION function_name用于删除现有的存储函数。

创建存储的例程

以下语法可用于创建存储过程:

CREATE
   [DEFINER = { user | CURRENT_USER }
   PROCEDURE procedure_name ([parameter[, ...]])
   [characteristics, ...] routine_body

而以下用于创建存储函数:

CREATE
   [DEFINER = { user | CURRENT_USER }
   FUNCTION function_name ([parameter[, ...]])
   RETURNS type
   [characteristics, ...] routine_body

例如,下面创建了一个返回静态字符串的简单存储过程:

mysql>CREATE PROCEDURE get_inventory()
    >

就这样。现在使用以下命令执行该过程:

mysql>CALL get_inventory();

执行此过程将返回以下输出:

+---------------+
| inventory     |
+---------------+
|        45     |
+---------------+

当然,这是一个很简单的例子。请继续阅读,了解更多关于创建更复杂(且有用)的存储例程的所有选项。

设置安全权限

DEFINER子句确定将咨询哪个用户帐户,以确定是否有适当的特权来执行由存储例程定义的查询。如果使用DEFINER子句,您需要使用'user@host'语法指定用户名和主机名(例如,'jason@localhost')。如果使用了CURRENT_USER(缺省值),那么将查询导致例程执行的任何帐户的特权。只有拥有SUPER权限的用户才能将DEFINER分配给另一个用户。

设置输入和返回参数

存储过程既可以接受输入参数,也可以将参数返回给调用者。但是,对于每个参数,您需要声明名称、数据类型,以及它是用于将信息传递到过程中,还是将信息传递回过程外,或者同时执行这两种功能。

注意

本节仅适用于存储过程。虽然存储函数可以接受参数,但它们只支持输入参数,并且必须返回一个且只有一个值。因此,在声明存储函数的输入参数时,请确保只包含名称和类型。

存储例程中支持的数据类型是 MySQL 支持的数据类型。因此,您可以自由地将参数声明为创建表时可能使用的任何数据类型。

要声明参数的用途,请使用以下三个关键字之一:

  • IN : IN参数仅用于将信息传递到程序中。

  • OUT : OUT参数仅用于将信息传回程序之外。

  • INOUT : INOUT参数可以将信息传递到过程中,更改其值,然后将信息传递回过程之外。

对于任何声明为OUTINOUT的参数,您需要在调用存储过程时在它的名称前加上@符号,以便可以从过程外部调用该参数。考虑一个指定名为get_inventory的过程的例子,该过程接受两个参数productid,一个IN参数确定您感兴趣的产品;和count,一个将值返回到调用者范围的OUT参数:

CREATE PROCEDURE get_inventory(IN product CHAR(8), OUT count INT)
  SELECT 45 INTO count;

这个过程可以这样调用:

CALL get_inventory("ZXY83393", @count);

可以这样访问count参数

SELECT @count;

在这种情况下,@count充当一个变量,只要会话处于活动状态,就可以访问它,或者直到被另一个值覆盖。

特征

称为特征的几个属性允许您调整存储过程的行为。下面列出了完整的特性范围,并对每个特性进行了介绍:

LANGUAGE SQL
| [NOT] DETERMINISTIC
| { CONTAINS SQL | NO SQL | READS SQL DATA | MODIFIES SQL DATA }
| SQL SECURITY {DEFINER | INVOKER}
| COMMENT 'string'

语言 SQL

目前,SQL 是唯一受支持的存储过程语言,但是有计划在将来引入一个框架来支持其他语言。这个框架将被公开,这意味着任何有意愿和能力的程序员都可以自由地添加对他喜欢的语言的支持。例如,您很可能能够使用 PHP、Perl 和 Python 等语言创建存储过程,这意味着这些过程的功能将只受所使用语言的限制。

[不]确定性

仅用于存储函数,任何声明为DETERMINISTIC的函数每次都将返回相同的值,只要传入相同的参数集。声明函数DETERMINISTIC有助于 MySQL 优化存储函数的执行,并有助于复制场景。

包含 SQL |无 SQL |读取 SQL 数据|修改 SQL 数据

此设置指示存储过程将执行什么类型的任务。默认值CONTAINS SQL指定 SQL 存在,但不会读取或写入数据。NO SQL表示过程中没有 SQL。READS SQL DATA表示 SQL 将只检索数据。最后,MODIFIES SQL DATA表示 SQL 将修改数据。在编写本文时,这个特性对存储过程的功能没有任何影响。

SQL SECURITY {DEFINER | INVOKER}

如果SQL SECURITY特性设置为DEFINER,那么程序将根据定义程序的用户的权限执行。如果设置为INVOKER,它将根据执行程序的用户的权限来执行。

你可能会认为DEFINER的设定有点奇怪,也许不安全。毕竟,为什么有人会允许一个用户使用另一个用户的特权来执行过程呢?这实际上是一种加强而不是放弃系统安全性的好方法,因为它允许您创建除了执行这些过程之外对数据库没有任何权限的用户。

字符串 COMMENT

您可以通过使用COMMENT特性添加一些关于程序的描述性信息。

声明和设置变量

在存储例程中执行任务时,通常需要局部变量作为临时占位符。然而,与 PHP 不同,MySQL 要求您指定变量的类型并显式声明它们。本节将向您展示如何声明和设置变量。

声明变量

与 PHP 不同,MySQL 要求您在使用本地变量之前在存储例程中声明本地变量,通过使用 MySQL 支持的数据类型之一来指定它们的类型。用DECLARE语句确认变量声明,其原型如下所示:

DECLARE variable_name type [DEFAULT value]

例如,假设创建了一个名为calculate_bonus的存储过程来计算雇员的年度奖金。它可能需要一个名为salary的变量,另一个名为bonus,第三个名为total。他们会这样宣布:

DECLARE salary DECIMAL(8,2);
DECLARE bonus DECIMAL(4,2);
DECLARE total DECIMAL(9,2);

当声明变量时,声明必须发生在一个BEGIN/END块中,如本章稍后所述。此外,声明必须在执行该块中的任何其他语句之前发生。还要注意变量作用域被限制在声明它的块中,这很重要,因为在一个例程中可能有几个BEGIN / END块。

关键字DECLARE也用于声明某些条件和处理器。这个问题在“条件和处理器”一节中有更详细的讨论。

设置变量

SET语句用于设置已声明的存储例程变量的值。它的原型是这样的:

SET variable_name = value [, variable_name = value]

以下示例说明了声明和设置名为inv的变量的过程:

DECLARE inv INT;
SET inv = 155;

也可以使用SELECT INTO语句设置变量。例如,inv变量也可以这样设置:

DECLARE inv INT;
SELECT inventory INTO inv FROM product WHERE productid="MZC38373";

该变量在声明它的BEGIN / END块的范围内是局部的。如果你想在例程之外使用这个变量,你需要把它作为一个OUT变量传入,就像这样:

mysql>DELIMITER //
mysql>CREATE PROCEDURE get_inventory(OUT inv INT)
->SELECT 45 INTO inv;
->//
Query OK, 0 rows affected (0.08 sec)
mysql>DELIMITER ;
mysql>CALL get_inventory(@inv);
mysql>SELECT @inv;

这将返回以下内容:

+-------------+
| @inv        |
+-------------+
| 45          |
+-------------+

您可能对DELIMITER语句感到疑惑。默认情况下,MySQL 使用分号来确定语句何时结束。然而,当创建一个多语句存储例程时,您需要编写几个语句,但是您不希望 MySQL 做任何事情,直到您完成了存储例程的编写。因此,您必须将分隔符更改为另一个字符串。不一定非得是//。你可以选择任何你喜欢的,例如,|||^^

执行存储的例程

通过结合使用CALL语句引用存储的例程,可以执行存储的例程。例如,执行先前创建的get_inventory过程是这样完成的:

mysql>CALL get_inventory(@inv);
mysql>SELECT @inv;

执行get_inventory将返回:

+-------------+
| @inv        |
+-------------+
| 45          |
+-------------+

创建和使用多语句存储例程

单语句存储例程非常有用,但是存储例程的真正强大之处在于它们能够封装和执行多条语句。事实上,整个语言都由您支配,使您能够执行相当复杂的任务,如条件求值和迭代。例如,假设你公司的收入是由销售人员推动的。为了鼓励员工实现其崇高的目标,年终会发放奖金,奖金的多少与员工的收入成正比。该公司在内部处理其工资单,使用一个定制的 Java 程序在每年年底计算并打印奖金支票;然而,用 PHP 和 MySQL 创建的基于 web 的界面提供给销售人员,以便他们可以监控其进度(和奖金数额)。因为这两个应用都需要计算奖金数额的能力,所以这个任务似乎是存储函数的理想选择。创建此存储过程的语法如下所示:

DELIMITER //
CREATE FUNCTION calculate_bonus
(emp_id CHAR(8)) RETURNS DECIMAL(10,2)
COMMENT 'Calculate employee bonus'
BEGIN
   DECLARE total DECIMAL(10,2);
   DECLARE bonus DECIMAL(10,2);
   SELECT SUM(revenue) INTO total FROM sales WHERE employee_id = emp_id;
   SET bonus = total * .05;
   RETURN bonus;
END;
//
DELIMITER ;

然后像这样调用calculate_bonus函数:

mysql>SELECT calculate_bonus("35558ZHU");

该函数返回类似如下的内容:

+-----------------------------+
| calculate_bonus("35558ZHU") |
+-----------------------------+
|                      295.02 |
+-----------------------------+

尽管这个例子包括了一些新的语法(所有这些都将很快被介绍),但是它应该是相当简单的。

本节的其余部分将专门介绍创建多语句存储例程时常用的语法。

有效的存储例行管理

存储例程会很快变得冗长而复杂,增加了创建和调试它们的语法所需的时间。例如,键入calculate_bonus过程可能会很乏味,特别是如果在这个过程中您引入了一个语法错误,需要重新输入整个例程。为了减轻一些繁琐,将存储的例程创建语法插入到一个文本文件中,然后将该文件读入到mysql客户端,如下所示:

%>mysql [options] < calculate_bonus.sql

使用 GUI 客户机将允许您编辑过程并重新提交它,直到您获得正确的语法和业务逻辑,而不必每次都重新开始。

[options]字符串是连接变量的占位符。在创建例程之前,不要忘记通过在脚本顶部添加USE db_name ;来切换到适当的数据库;否则,将会出现错误。

要修改现有的 routine,您可以根据需要更改文件,使用DROP PROCEDURE(本章稍后介绍)删除现有的 routine,然后使用上述过程重新创建它。虽然有一个ALTER PROCEDURE语句(也将在本章后面介绍),但它目前只能修改例程特性。

管理例程的另一个非常有效的机制是通过 MySQL Workbench,可以从 MySQL 下载。通过该界面,您可以创建、编辑和删除例程。

开始和结束块

创建多语句存储例程时,需要将语句放在一个BEGIN / END块中。块原型看起来像这样:

BEGIN
   statement 1;
   statement 2;
   ...
   statement N;
END

请注意,块中的每条语句都必须以分号结束。

条件式

基于运行时信息的任务执行是对结果进行严格控制的关键。存储例程语法为执行条件求值提供了两个众所周知的构造:IF-ELSEIF-ELSE语句和CASE语句。这两者都在本节中介绍。

多个分支

IF-ELSEIF-ELSE语句是评估条件语句最常用的方法之一。事实上,即使你是一个程序员新手,你也可能已经在很多场合使用过它。所以这个介绍应该是比较熟悉的。原型看起来像这样:

IF condition THEN statement_list
   [ELSEIF condition THEN statement_list]
   [ELSE statement_list]
END IF

例如,假设您修改了先前创建的calculate_bonus存储过程,不仅根据销售额,还根据销售人员在公司的工作年限来确定奖金百分比:

IF years_employed < 5 THEN
   SET bonus = total * .05;
ELSEIF years_employed >= 5 and years_employed < 10 THEN
   SET bonus = total * .06;
ELSEIF years_employed >=10 THEN
   SET bonus = total * .07;
END 

IF

情况

当您需要将一个值与一系列可能性进行比较时,CASE语句非常有用。虽然使用IF语句这样做当然是可能的,但是使用CASE语句可以大大提高代码的可读性。它的原型是这样的:

CASE
   WHEN condition THEN statement_list
   [WHEN condition THEN statement_list]
   [ELSE statement_list]
END CASE

考虑下面的示例,该示例通过将客户所在的州与一组值进行比较来设置包含适当销售税率的变量:

CASE
   WHEN state="AL" THEN:
      SET tax_rate = .04;
   WHEN state="AK" THEN:
      SET tax_rate = .00;
   ...
   WHEN state="WY" THEN:
      SET tax_rate = .04;
END CASE;

或者,您可以通过使用以下变体来节省一些输入:

CASE state
   WHEN "AL" THEN:
      SET tax_rate = .04;
   WHEN "AK" THEN:
      SET tax_rate = .00;
   ...
   WHEN "WY" THEN:
      SET tax_rate = .04;

END 

CASE;

循环

有些任务,比如向表中插入一些新行,需要能够重复执行一组语句。本节介绍了可用于迭代和退出循环的各种方法。

重复

执行ITERATE语句会导致嵌入该语句的LOOPREPEATWHILE块返回顶部并再次执行。它的原型是这样的:

ITERATE label

考虑一个例子。以下存储过程将使每个雇员的工资增加 5 %,但雇员类别为 0 的雇员除外:

DELIMITER //

DROP PROCEDURE IF EXISTS `corporate`.`calc_bonus`//
CREATE PROCEDURE `corporate`.`calc_bonus` ()
BEGIN

DECLARE empID INT;
DECLARE emp_cat INT;
DECLARE sal DECIMAL(8,2);
DECLARE finished INTEGER DEFAULT 0;

DECLARE emp_cur CURSOR FOR
   SELECT employee_id, salary FROM employees ORDER BY employee_id;

DECLARE CONTINUE HANDLER FOR NOT FOUND SET finished=1;

OPEN emp_cur;

calcloop: LOOP

   FETCH emp_cur INTO empID, emp_cat;

   IF finished=1 THEN
      LEAVE calcloop;
   END IF;

   IF emp_cat=0 THEN
      ITERATE calcloop;
   END IF;

   UPDATE employees SET salary = salary * 1.05 WHERE employee_id=empID;

END LOOP calcloop;

CLOSE emp_cur;

END//

DELIMITER ;

请注意,游标用于遍历结果集的每一行。如果您不熟悉该功能,请参见第三十二章。

离开

在等待某个变量的值或某个特定任务的结果时,您可能希望使用LEAVE命令立即退出一个循环或BEGIN / END块。其原型如下:

LEAVE label

LOOP section中提供了一个LEAVE运行的例子。您还会在ITERATE示例中找到LEAVE

LOOP语句将继续迭代其块中定义的一组语句,直到遇到LEAVE语句。其原型如下:

[begin_label:] LOOP
   statement_list
END LOOP [end_label]

MySQL 存储例程不能接受数组作为输入参数,但是您可以通过传入和解析分隔字符串来模拟这种行为。例如,假设您为客户提供了一个界面,让他们从 10 个他们想了解更多的公司服务中进行选择。界面可以呈现为多选框、复选框或一些其他机制;使用哪一个并不重要,因为值的数组最终会在传递给存储例程之前被压缩成一个字符串(例如,使用 PHP 的implode()函数)。例如,字符串可能如下所示,每个数字代表所需服务的数字标识符:

1,3,4,7,8,9,10

为解析该字符串并将值插入数据库而创建的存储过程可能如下所示:

DELIMITER //

CREATE PROCEDURE service_info
(IN client_id INT, IN services varchar(20))

   BEGIN

      DECLARE comma_pos INT;
      DECLARE current_id INT;

      svcs: LOOP

         SET comma_pos = LOCATE(',', services);
         SET current_id = SUBSTR(services, 1, comma_pos);

         IF current_id <> 0 THEN
            SET services = SUBSTR(services, comma_pos+1);
         ELSE
            SET current_id = services;
         END IF;

         INSERT INTO request_info VALUES(NULL, client_id, current_id);

         IF comma_pos = 0 OR current_id = “ THEN
            LEAVE svcs;
         END IF;

      END LOOP;

   END//
DELIMITER ;

现在叫service_info,像这样:

call service_info("45","1,4,6");

一旦执行,request_info表将包含以下三行:

+-------+----------+----------+
| row_id | client_id | service |
+-------+----------+----------+
|     1  |       45 |       1  |
|     2  |       45 |       4  |
|     3  |       45 |       6  |
+-------+----------+----------+

重复

REPEAT语句的操作与WHILE几乎相同,只要某个条件为真,就在指定的语句或语句集上循环。然而,与WHILE不同的是,REPEAT在每次迭代之后而不是之前评估条件,这使得它类似于 PHP 的DO WHILE构造。其原型如下:

[begin_label:] REPEAT
   statement_list
UNTIL condition
END REPEAT [end_label]

例如,假设您正在测试一组新的应用,并希望构建一个存储过程,用给定数量的测试行填充一个表。程序如下:

DELIMITER //
CREATE PROCEDURE test_data
(rows INT)
BEGIN

   DECLARE val1 FLOAT;
   DECLARE val2 FLOAT;

   REPEAT
      SELECT RAND() INTO val1;
      SELECT RAND() INTO val2;
      INSERT INTO analysis VALUES(NULL, val1, val2);
      SET rows = rows - 1;
   UNTIL rows = 0
   END REPEAT;

END//

DELIMITER ;

执行这个过程,传入一个参数为 5 的rows会产生以下结果:

+--------+-----------+----------+
| row_id | val1      | val2     |
+--------+-----------+----------+
|     1  | 0.0632789 | 0.980422 |
|     2  |  0.712274 | 0.620106 |
|     3  |  0.963705 | 0.958209 |
|     4  |  0.899929 | 0.625017 |
|     5  |  0.425301 | 0.251453 |
+--------+-----------+----------+

正在…

WHILE语句在许多(如果不是全部的话)现代编程语言中很常见,只要特定的条件或条件集保持为真,就迭代一个或几个语句。其原型如下:

[begin_label:] WHILE condition DO
   statement_list
END WHILE [end_label]

在上面对REPEAT的介绍中首次创建的test_data程序已经被重写,这次使用了一个WHILE循环:

DELIMITER //
CREATE PROCEDURE test_data
(IN rows INT)
BEGIN

   DECLARE val1 FLOAT;
   DECLARE val2 FLOAT;
   WHILE rows > 0 DO
      SELECT RAND() INTO val1;
      SELECT RAND() INTO val2;
      INSERT INTO analysis VALUES(NULL, val1, val2);
      SET rows = rows - 1;
   END WHILE;

END//

DELIMITER ;

执行该程序会产生与REPEAT部分所示类似的结果。

从一个例程中调用另一个例程

从一个例程中调用另一个例程是可能的,这样可以省去不必要地重复逻辑的麻烦。下面是一个例子:

DELIMITER //
CREATE PROCEDURE process_logs()
BEGIN
   SELECT "Processing Logs";
END//

CREATE PROCEDURE process_users()
BEGIN
   SELECT "Processing Users";
END//

CREATE PROCEDURE maintenance()
BEGIN
   CALL process_logs();
   CALL process_users();
END//

DELIMITER ;

执行maintenance()程序会产生以下结果:

+-----------------+
| Processing Logs |
+-----------------+
| Processing Logs |
+-----------------+
1 row in set (0.00 sec)

+------------------+
| Processing Users |
+------------------+
| Processing Users |
+------------------+
1 row in set (0.00 sec)

修改存储的程序

目前,MySQL 只提供通过ALTER语句修改存储的例程特性的能力。其原型如下:

ALTER (PROCEDURE | FUNCTION) routine_name [characteristic ...]

例如,假设您想要将calculate_bonus方法的SQL SECURITY特性从默认的DEFINER更改为INVOKER:

ALTER PROCEDURE calculate_bonus SQL SECURITY 

invoker;

删除存储的程序

要删除存储的程序,执行DROP语句。其原型如下:

DROP (PROCEDURE | FUNCTION) [IF EXISTS] routine_name

例如,要删除calculate_bonus存储过程,请执行以下命令:

mysql>DROP PROCEDURE calculate_bonus;

您将需要ALTER ROUTINE特权来执行DROP

查看例程的状态

有时,您可能有兴趣了解更多关于谁创建了一个特定的例程,例程的创建或修改时间,或者例程应用于什么数据库。这很容易用SHOW STATUS语句来完成。它的原型是这样的:

SHOW (PROCEDURE | FUNCTION) STATUS [LIKE 'pattern']

例如,假设您想了解关于以前创建的get_products()存储过程的更多信息:

mysql>SHOW PROCEDURE STATUS LIKE 'get_products'\G

执行此命令会产生以下输出:

*************************** 1\. row ***************************
                  Db: corporate
                Name: get_products
                Type: PROCEDURE
             Definer: root@localhost
            Modified: 2018-08-08 21:48:20
             Created: 2018-08-08 21:48:20
       Security_type: DEFINER
             Comment:
character_set_client: utf8
collation_connection: utf8_general_ci
  Database Collation: latin1_swedish_ci
1 row in set (0.01 sec)

请注意,\G选项用于以垂直格式而非水平格式显示输出。忽略包含\G会产生横向的结果,可能难以阅读。

如果您想同时查看关于几个存储例程的信息,也可以使用通配符。例如,假设另一个名为get_employees()的存储例程可用:

mysql>SHOW PROCEDURE STATUS LIKE 'get_%'\G

这将产生:

*************************** 1\. row ***************************
                  Db: corporate
                Name: get_employees
                Type: PROCEDURE
             Definer: root@localhost
            Modified: 2018-08-08 21:48:20
             Created: 2018-08-08 21:48:20
       Security_type: DEFINER
             Comment:
character_set_client: utf8
collation_connection: utf8_general_ci
  Database Collation: latin1_swedish_ci
*************************** 2\. row ***************************
                  Db: corporate
                Name: get_products
                Type: PROCEDURE
             Definer: root@localhost
            Modified: 2018-08-08 20:12:39
             Created: 2018-08-08 22:12:39
       Security_type: DEFINER
             Comment:
character_set_client: utf8
collation_connection: utf8_general_ci
  Database Collation: latin1_swedish_ci
2 row in set (0.02 sec)

查看例程的创建语法

通过使用SHOW CREATE语句,可以查看用于创建特定例程的语法。其原型如下:

SHOW CREATE (PROCEDURE | FUNCTION) dbname.spname

例如,以下语句将重新创建用于创建get_products()过程的语法:

SHOW CREATE PROCEDURE corporate.maintenance\G

执行此命令会产生以下输出(为便于阅读,稍微进行了格式化):

*************************** 1\. row ***************************
Procedure: maintenance
sql_mode: STRICT_TRANS_TABLES,NO_AUTO_CREATE_USER

Create Procedure: CREATE DEFINER=`root`@`localhost` PROCEDURE `maintenance`()
BEGIN
    CALL process_logs();
    CALL process_users();
END

character_set_client: latin1
collation_connection: latin1_swedish_ci
Database Collation: latin1_swedish_ci

处理条件

本章早些时候提到过,DECLARE语句还可以指定在特定情况或条件发生时可以执行的处理器。例如,在calc_bonus过程中使用了一个处理器来确定结果集的迭代何时完成。需要两个声明:一个名为finished的变量和一个用于NOT FOUND条件的处理器:

DECLARE finished INTEGER DEFAULT 0;
DECLARE CONTINUE HANDLER FOR NOT FOUND SET finished=1;

一旦进入迭代循环,每次迭代都检查finished,如果它被设置为 1,循环将被退出:

IF finished=1 THEN
   LEAVE calcloop;
END IF;

MySQL 支持许多可以根据需要做出反应的条件。有关更多详细信息,请参见 MySQL 文档。

将例程集成到 Web 应用中

到目前为止,所有的例子都是通过 MySQL 客户端演示的。虽然这肯定是测试示例的有效方法,但是存储例程的效用会因为能够将它们合并到您的应用中而大大增加。这一节演示了将存储例程集成到 PHP 驱动的 web 应用中是多么容易。

创建员工奖金界面

回到涉及员工奖金计算的多语句存储函数示例,上面提到提供了一个基于 web 的界面,使员工能够实时跟踪他们的年度奖金。这个例子演示了使用calculate_bonus()存储函数来完成这个任务是多么容易。

清单 29-1 展示了用于提示输入员工 ID 的简单 HTML 表单。当然,在现实世界中,这种表单也会要求输入密码;然而,对于这个例子来说,ID 就足够了。

<form action="viewbonus.php" method="post">
   Employee ID:<br>
   <input type="text" name="employeeid" size="8" maxlength="8" value="">
   <input type="submit" value="View Present Bonus">
</form>

Listing 29-1The Employee Login Form (login.php

)

清单 29-2 接收login.php提供的信息,使用提供的员工 ID 和calculate_bonus()存储的函数计算并显示奖金信息。

<?php

    // Instantiate the mysqli class
    $db = new mysqli("localhost", "websiteuser", "jason", "corporate");

    // Assign the employeeID
    $eid = filter_var($_POST['employeeid'], FILTER_SANITIZE_NUMBER_INT);

    // Execute the stored procedure
    $stmt = $db->prepare("SELECT calculate_bonus(?) AS bonus");

    $stmt->bind_param('s', $eid);

    $stmt->execute();

    $stmt->bind_result($bonus);

    $stmt->fetch();

   printf("Your bonus is \$%01.2f",$bonus);
?>

Listing 29-2Retrieving the Present Bonus Amount (viewbonus.php

)

Executing this example produces output similar to this:

Your bonus is $295.02

检索多行

虽然上面的例子足以理解如何从一个存储例程返回多行,但是下面的简单例子非常清楚地说明了这一点。假设您创建了一个存储过程来检索有关公司雇员的信息:

CREATE PROCEDURE get_employees()
   SELECT employee_id, name, position FROM employees ORDER by name;

然后可以从 PHP 脚本中调用这个过程,如下所示:

<?php
  // Instantiate the mysqli class
  $db = new mysqli("localhost", "websiteuser", "jason", "corporate");

  // Execute the stored procedure
  $result = $db->query("CALL get_employees()");

  // Loop through the results
  while (list($employee_id, $name, $position) = $result->fetch_row()) {
     echo "$employee_id, $name, $position <br>";
  }

?>

执行此脚本会产生类似于以下内容的输出:

EMP12388, Clint Eastwood, Director
EMP76777, John Wayne, Actor
EMP87824, Miles Davis, Musician

摘要

本章介绍了存储例程。您了解了在决定是否应该将这个特性合并到您的开发策略中时要考虑的优点和缺点。您还了解了 MySQL 的具体实现和语法。最后,您了解了将存储函数和存储过程合并到 PHP 应用中是多么容易。

下一章介绍 MySQL 和 MariaDB 中的另一个特性:触发器。