PHP 通过PDO链接ODBC
PHP 5.1 发布时将附带一个全新的数据库连接层,即 PHP Data Objects (PDO)。虽然 PHP 一直都拥有很好的数据库连接,但 PDO 让 PHP 达到一个新的高度。学习如何获得、安装和使用 PDO,以连接到 IBM® DB2® Universal Database™ 和 IBM Cloudscape™ 数据库,插入和检索数据,并探索更多高级特性,
例如:
预处理语句(prepared statements)、
绑定参数(bound parameters)、
可滚动游标(scrollable cursors)、
定位更新(positioned updates)
以及 LOB。
另外,本文将简要地介绍一下对多字节数据的处理。
背景
随着拥有更成熟 OO 语法的 PHP 5 的发布,PHP 越来越多地受到越来越大的机构的关注,对于 PHP 来说,提供更加一致的和可访问的数据访问 API 变得越来越重要。
PHP 与流行的开放源代码关系数据库管理系统(RDBMS)MySQL 之间总是很有默契。这对拍档的成功很大程度上是由于它们免费可用,而且进入的门槛也比较低,这两种产品的合作使它们各自都取得了广受推崇的地位。
很多 PHP 应用程序开发人员都习惯于 PHP-MySQL 这对组合,以致 PHP 对其他数据库的支持常常模仿 MySQL 客户机库 API。然而,并不是所有的数据库客户机 API 都是一样的,也不是所有的数据库都提供相同的特性。虽然存在模仿,但不同的 PHP 数据库扩展都有它们各自的怪僻和不同之处,所以从一种数据库迁移到另一种数据库时会有一些困难。虽然这不是创建 PDO 的直接原因,但是在设计过程中还是有一定影响的。
如果您是带着想结合使用 PHP 和 DB2 的目的阅读本文,那么您很可能属于以下类型中的一种:
您从一家小公司开始,在 MySQL(举个例子)上运行 PHP,由于业务增长,您需要 DB2 所提供的可伸缩性/可靠性/支持或其他特性。您希望移植代码,以使用 DB2,但由于 API 的变化,您需要编写或实现一个抽象层,以便在 DB2 上测试应用程序的同时可以继续在旧的数据库上运行。不仅如此,您还希望能有自己的选择,并保留支持其他 RDBMS 的可能性,因为您清楚,有些客户机可能已经和其他平台栓在一起了。
您用 PHP 在 MySQL之上构建了一个小型的部门应用程序(同样,这只是举个例子,我并不是要跟 MySQL 过不去)。事实证明这个应用程序本身很有用,现在已经在这个部门之外使用,并且闯入了 CIO/CTO 的法眼 —— 现在需要遵从托管的标准数据库。(是的,这是第一点的一个变种。)
在其他某些复杂的企业级应用程序的后台,您已经有一个 DB2 实例;您希望利用 PHP 的快速应用程序开发和原型设计来生成动态报告。
目标
至此我们已经掌握了数据库及 PHP 的背景知识,现在正好可以提及 PDO 背后的一些设计目标:
为大多数数据库 API 中的常见特性提供一致的 API。
具有可扩展性,以使数据库供应商 X 仍然可以暴露特性 Y 并保持 PDO 的兼容性。
提供大量基本的兼容性技巧,以便能够更方便地创建跨数据库兼容的应用程序。
不为给定数据库 API 中本来没有的特性(例如序列)提供完全抽象或仿真。PDO 类意图为您提供对数据库本地特性的一致性访问,并减少干扰。
通过将与 PHP 内部打交道的代码(这是最难于编写的部分)集中起来,简化 PHP 数据库驱动程序的创建。
最后一点非常重要。PDO 是模块化结构,它被分成一个公共核心以及一个或多个驱动程序扩展,公共核心提供了在脚本(PDO 本身)中使用的 API,驱动程序扩展则为 PDO 和本地 RDBMS 客户机 API 库架起一座桥梁。DB2 用户将会希望使用 PDO_ODBC 驱动程序,据称它可以提供以下特性:
它经过重新编写,能支持遵从 ODBC V3 的驱动程序和驱动程序管理器。它还考虑了对 DB2 特定特性和优化的支持,这成为设计过程中的一部分 —— 不是后来补充的。
它支持经过试验和测试的存储过程和大型对象。它不仅能够工作,而且非常好用。
对于取 10,000 行记录这样的 DB2 访问操作,使用 PDO_ODBC 驱动程序时的性能比使用传统的 PHP Unified ODBC 扩展要快大约 10 倍。之所以有这么大的差异,是因为在 PDO 中默认的游标是轻量级的只能向前移动的游标。
获取和安装 PDO
PHP 5.1 发布时将附带 PDO,但是也可以通过 PECL 这个 PHP 扩展库(PHP Extension Repository)来结合使用 PDO 和 PHP 5.0.3 及以上版本。如果您使用的是 Windows®,那么您会欣喜地发现安装过程要简单得多。
我将假设您已经拥有配置 PHP 5 使之使用您选择的 Web 服务器的经验,只有在此假设下,我才能集中精力关注更相关的细节。同样,我还将假设您使用的是一个 DB2 Universal Database 服务器或网络服务器模式下的 IBM Cloudscape 数据库,并且接受了用户为 db2inst1、密码为 ibmdb2 的默认安装选项。如果您自己编译驱动程序,那么在进行编译的机器上,应该安装有 DB2 客户机,并且存在应用程序开发 header,否则编译将遭到失败。
在 PHP 5.0.3 及以上版本上通过 PECL 进行安装
默认情况下,PHP 将安装 "PEAR" 包管理系统。您选择的 OS 发行版很可能已经创建了一个包含 PEAR 的组件的包,很可能您已经安装了这个包,并准备运行它。让我们试验一下。
如果它不能工作
如果您没能看到类似于左侧文本的输出,那么很可能您没有安装需要的所有包。这时应查阅您选择的 OS 发行版的文档,看看接下来应该做什么。或者,您可以自己编译 PHP。
清单 1. 列出已安装的 PEAR 包
$ pear list
Installed packages:
===================
Package Version State
Archive_Tar 1.1 stable
Console_Getopt 1.2 stable
PEAR 1.3.4 stable
XML_RPC 1.1.0 stable
这个包列表表明,我已经安装了 PEAR 1.3.4。很可能您也会安装那个版本。为了成功地安装 PDO,需要升级到 PEAR 1.3.5;这个过程很快,很顺利:
$ sudo pear upgrade PEAR
现在便可以放心安装 PDO 了:
$ sudo pear install PDO
您已经安装了 PDO 核心,为了使之生效,需要在 php.ini 文件中启用它。您需要添加以下一行:
extension=pdo.so
现在安装用于 PDO 的 ODBC 驱动程序,如果您需要连接到 DB2、Cloudscape 或 Apache Derby,就需要这个驱动程序:
$ sudo pear install PDO_ODBC
您将看到这样的提示:flavour,dir ? (just leave blank for help)。这是一个稍微有点隐蔽的提示,它询问需要配置哪种类型的 ODBC 驱动程序,以及将它安装在哪里。如果在安装 DB2 时选择了默认安装选项,那么可以输入 ibm-db2,这相当于 ibm-db2,/home/db2inst1/sqllib。如果您选择了不同的安装位置,那么应该用它置替换 /home/db2inst1/sqllib。输入了正确的细节后,按下 enter 键,这样驱动程序就会构建和安装。
您需要将驱动程序添加到 php.ini 文件中,从而激活驱动程序。确保将下面这一行添加在之前所添加的 pdo.so 这一行之后,否则 PHP 不能正确地初始化。
extension=pdo_odbc.so
PHP 5.1 及以上版本上的安装
PHP 5.1 发布时附带了 PDO。为获得 DB2 支持,只需将下面的开关添加到配置行。您显然希望添加更多的配置选项,以满足您自己的 Web 服务器。关于这方面的详细内容,可以查看 PHP 文档。我将假设您使用的是一个最近的 Linux® 发行版,并运行 Apache 2:
$ tar xjf php-5.1.0.tar.bz2
$ cd php-5.1.0
$ ./configure --with-pdo-odbc=ibm-db2,/home/db2inst1/sqllib \
--with-apxs2=/usr/sbin/apxs
$ make
$ sudo make install
Windows 上的安装
Windows 上的安装比起 UNIX® 上的安装来要简单一些。如果您下载了 PHP 5.1,那么就已经拥有了这个包中相关的 DLL。否则,您需要从 PHP 快照站点(请参阅本文后面的“下载”小节)下载这些 DLL。
为了激活 PDO,将下面两行添加到 php.ini 文件:
extension=php_pdo.dll
extension=php_pdo_odbc.dll
重新启动 Web 服务器
安装完毕后,应该完全重新启动 Web 服务器,以确保 PHP 装载新的扩展,这样就可以开始使用 PDO 了。如果您使用的是 UNIX 平台,那么需要获得 DB2 客户机的 DB2 实例环境,以便正确地初始化。如果您使用的是 bourne shell 型的 shell,那么可以通过运行命令 . /home/db2inst1/sqllib/db2profile 来获得。(注意: 假定开头部分的句号已经在那里!)您需要作出安排,使之在 Web 服务器启动脚本中自动发生。
PDO 中的关键概念
为了掌握 PDO,需要了解 5 个关键概念。这 5 个概念是:
连接和连接管理
事务和自动提交
预处理语句和存储过程
错误和错误处理
特定于驱动程序的功能性,包括 滚动游标 和 大型对象
连接和连接管理
连接是通过创建 PDO 基类的实例而建立的。不管您想要使用哪种驱动程序,您总是使用 PDO 类名。构造函数接受用于指定数据源(即 DSN)的参数,可能还包括用户名和密码参数(如果有的话)。最后一个参数用于传递附加的调优参数到 PDO 或底层驱动程序 —— 后面很快会有更详细的论述。下面是一个简短的连接到 DB2 的示例脚本:
清单 2. 如何使用 PDO 连接到 DB2
try {
$dbh = new PDO('odbc:SAMPLE', 'db2inst1', 'ibmdb2');
echo "Connected\n";
} catch (Exception $e) {
echo "Failed: " . $e->getMessage();
}
odbc:SAMPLE 告诉 PDO 它应该使用 ODBC 驱动程序,并且应该使用 "SAMPLE" 数据库。如果使用一个驱动程序管理器,那么可以用一个 ODBC 级数据源名称替代 SAMPLE。实际上,在冒号字符之后可以指定任何有效的 ODBC 数据源连接字符串。
如果连接成功,您将看到消息 "Connected",否则,PDO 将抛出一个 PDOException,解释为什么连接失败。可能的原因包括无效的参数,不正确的用户/密码,甚至是您忘了装载驱动程序。
值得注意的是,除非您捕捉从构造函数抛出的异常,否则,如果 PHP 脚本未能连接到数据库,它将终止。这与传统的 PHP 数据库扩展有很大的不同。对于不喜欢异常的人来说,只有两个“硬故障(hard-failure)”点可能抛出异常,这是其中一个点(另一个地点是,当您 试图使用事务时缺乏对事务的支持)。对于所有其他错误,PDO 将使用您选择的 错误处理设置。
连接将保持开放状态,直到所有对它的引用被释放。如果在主脚本的顶端打开连接, 并将其句柄存储在一个全局变量中,那么该连接将一直处于开放状态,直到脚本结束,或者直到 $dbh 变量被设为 null。如果在一个函数中打开连接,并且只将句柄存储在一个本地变量中,那么当函数返回时,连接将被关闭。这些语义对于 PHP 中的任何对象都是一样的,没有什么特别的地方。
ODBC 连接池
如果您使用的是 Windows,或者如果您选择在 UNIX 型平台上使用一个 ODBC 驱动程序管理器,那么值得注意的是,PDO_ODBC 将自动尝试使用该驱动程序管理器的 ODBC 连接池特性。这个特性类似于 PHP 级连接缓存,不要求专门请求一个持久的连接。此外,缓存是在 ODBC 级进行的,这意味着在同一个进程中运行的其他组件(例如在 IIS 下运行的 ASP/.Net 脚本)也能利用相同的连接池。
对于流量较大的站点,让 PHP 在不同请求的间隙中缓存打开的连接,使得每个进程(每个惟一的连接参数集)只需花费一次建立连接的成本,这样做常常很有益处。虽然这听起来像是一个不错的 想法,但您应该仔细评估这样做对系统的影响,因为当大量缓存的连接空闲在那里的时候,就会适得其反。
要建立一个缓存的连接(如果您更熟悉传统的数据库扩展的话,也可以说是 *pconnect()),需要在实例化数据库连接时传递一个属性:
清单 3. 如何用 PDO 连接到 DB2,使用持久(缓存)连接
try {
$dbh = new PDO('odbc:SAMPLE', 'db2inst1', 'ibmdb2',
array(PDO_ATTR_PERSISTENT => true));
echo "Connected\n";
} catch (Exception $e) {
echo "Failed: " . $e->getMessage();
}
事务和自动提交
至此,您已经通过 PDO 连接到了 DB2,在发出查询之前,您应该理解 PDO 是如何管理事务的。如果之前没有接触过事务,那么首先要知道事务的 4 个特征:原子性(Atomicity)、一致性(Consistency)、独立性(Isolation)和持久性(Durability),即 ACID。用外行人的话说,对于在一个事务中执行的任何工作,即使它是分阶段执行的,也一定可以保证该工作会安全地应用于数据库,并且在工作被提交时,不 会受到来自其他连接的影响。事务性工作可以根据请求自动撤销(假设您还没有提交它),这使得脚本中的错误处理变得更加容易。
事务通常是通过把一批更改积蓄起来、使之同时生效而实现的。这样做的好处是可以大大提高这些更新的效率。换句话说,事务可以使脚本更快,而且可能更健壮(不过需要正确地使用事务才能获得这样的好处)。
警告
只有在通过 PDO::beginTransaction() 启动事务的情况下,才会发生自动回滚。如果手动地发出开始一个事务的查询,那么 PDO 就无法知道该事务,从而不能在必要时进行回滚。
不幸的是,并不是每种数据库都支持事务,所以当第一次打开连接时,PDO 需要在所谓的“自动提交(auto-commit)”模式下运行。自动提交模式意味着,如果数据库支持事务,那么您所运行的每一个查询都有它自己的隐式事 务,如果数据库不支持事务,每个查询就没有这样的事务。如果您需要一个事务,那么必须使用 PDO::beginTransaction() 方法来启动一个事务。如果底层驱动程序不支持事务,那么将会抛出一个 PDOException(无论错误处理设置是怎样的:这总是一个严重错误状态)。在一个事务中,可以使用 PDO::commit() 或 PDO::rollBack() 来结束该事务,这取决于事务中运行的代码是否成功。
DB2 特性
虽然我认为事务通常要更快一些,但您还是应该自己评估事务是否真的可以加快代码。例如,在高并发环境中您可能会发现,过度使用事务会增加锁开销。如果在应 用程序中出现这种情况,那么建议的补救办法是在一般情况下使用自动提交,而对于真正需要全部 ACID 特征的代码部分则仍然使用事务。
当脚本结束时,或者当一个连接即将被关闭时,如果有一个未完成的事务,那么 PDO 将自动回滚该事务。这是一种安全措施,有助于避免在脚本非正常结束时出现不一致的情况 —— 如果没有显式地提交事务,那么假设有某个地方会出现不一致,所以要执行回滚,以保证数据的安全性。
清单 4. 在事务中执行批处理
try {
$dbh = new PDO('odbc:SAMPLE', 'db2inst1', 'ibmdb2',
array(PDO_ATTR_PERSISTENT => true));
echo "Connected\n";
$dbh->setAttribute(PDO_ATTR_ERRMODE, PDO_ERRMODE_EXCEPTION);
$dbh->beginTransaction();
$dbh->exec("insert into staff (id, first, last) values (23, 'Joe', 'Bloggs')");
$dbh->exec("insert into salarychange (id, amount, changedate)
values (23, 50000, NOW())");
$dbh->commit();
} catch (Exception $e) {
$dbh->rollBack();
echo "Failed: " . $e->getMessage();
}
在上面的示例中,假设我们为一个新雇员创建一组条目,这个雇员有一个 ID 号,即 23。除了输入这个人的基本数据外,我们还需要记录雇员的薪水。两个更新分别完成起来很简单,但通过将这两个更新包括在 beginTransaction() 和 commit() 调用中,就可以保证在更改完成之前,其他人无法看到更改。如果发生了错误,catch 块可以回滚事务开始以来发生的所有更改,并打印出一条错误消息。
并不是一定要在事务中作出更新。您也可以发出复杂的查询来提取数据,还可以使用那种信息构建更多的更新和查询。当事务在活动时,可以保证其他人在工作进行当中无法作出更改。事实上,这不是 100% 的正确,但如果您之前没有听说过事务的话,这样介绍也未尝不可。
关于 PHP 应用程序中安全性的说明
PHP Security Consortium
虽然本文表明在使用 PDO 时不再需要引用输入,但这不是说您应该盲目地使数据通过数据库。XSS 攻击是很实际的危险。您应该总是确保对传入应用程序的不受信任的数据应用适当的过滤器,并采取措施避免让不受信任的数据在站点上发出 HTML 或 javascript。
请访问 The PHP Security Consortium 以了解关于这些危险的更多知识,以及应该如何避免这些危险。
很多 PHP 脚本中一个常见的缺陷是缺乏输入检验。这种缺陷可以被利用,从而招致 XSS(Cross Site Scripting)以及 SQL 入侵攻击。在 SQL 入侵中,不受信任的数据(例如发给 Web 网页的反馈)和其他文本被衔接在一起,构成一个查询。攻击者可以蓄意地安排他们的输入,使之溢出引号之外,并在您想运行的真正查询后面链接上任意一个查询。这种攻击使攻击者可以更新、插入或删除数据,甚至可能可以看到数据库中的任意信息。
XSS 也是一个类似的问题。不过这一次不受信任的数据瞄准的是浏览站点的人们,而不是应用程序本身。通过提交包含 HTML 或 javascript 组合的文本,攻击者期望您之后会将那种数据直接输出到其他访问站点的人那里,从而使恶意代码可以在站点访问者的浏览器上运行。
在编写应用程序时,需要同时考虑这两种攻击。如果小心地检验和过滤输入,这两种攻击都是可以防止的。对 XSS 的处理很有技巧性,所以在这里我不便多讲(不过可以从侧栏找到有用的参考资料)。相比之下,SQL 入侵更容易对付。您只需在构造查询之前,适当地排除每块不受信任的数据。这种事情有点烦杂,特别是当您有大量的字段要处理时,很容易忘记做这件事。
虽然这是有用的(并且也是重要的)信息,但是您可能想知道,为什么我要花时间提到这一点,本文的重点不是结合使用 PDO 和 DB2 吗?原因是这样的:PHP 现在得到很广泛的部署,自然地,大量流行的基于 PHP 的应用程序也得到了广泛的部署。每当某一种这样的应用程序(和 PHP 本身没有联系)被发现存在漏洞时,PHP 常常被误认为是不安全的,可被利用的或者有缺陷的。为了避免将来出现这样的情况,我们可以采取的一个措施是鼓励应用程序开发人员多考虑安全问题,从而减少由诚实的错误导致的损害。扯远了,下面继续介绍其他关键概念。
预处理语句和存储过程
很多更成熟的数据库都支持预处理语句的概念。什么是预处理语句?您可以把预处理语句看作您想要运行的 SQL 的一种编译过的模板,它可以使用变量参数进行定制。预处理语句可以带来两大好处:
查询只需解析(或准备)一次,但是可以用相同或不同的参数执行多次。当查询准备好后,数据库将分析、编译和优化执行该查询的计划。对于复杂的查询,这个过程要花比较长的时间,如果您需要以不同参数多次重复相同的查询,那么该过程将大大降低应用程序的速度。通过使用预处理语句,可以避免重复分析/编译/优化周期。简言之,预处理语句使用更少的资源,因而运行得更快。
提供给预处理语句的参数不需要用引号括起来,驱动程序会处理这些。如果应用程序独占地使用预处理语句,那么可以确保没有 SQL 入侵发生。(然而,如果您仍然将查询的其他部分建立在不受信任的输入之上,那么就仍然存在风险)。
预处理语句是如此有用,以致 PDO 实际上打破了在目标 4 中设下的规则:如果驱动程序不支持预处理语句,那么 PDO 将仿真预处理语句。
下面是使用预处理语句的两个例子。第一个例子 通过替换指定占位符的 name 和 value,执行一次插入。而 第二个例子 使用问号占位符执行一条 select 语句。
清单 4. 使用预处理语句的重复插入
$stmt = $dbh->prepare("INSERT INTO REGISTRY (name, value) VALUES (:name, :value)");
$stmt->bindParam(':name', $name);
$stmt->bindParam(':value', $value);
// insert one row
$name = 'one';
$value = 1;
$stmt->execute();
// insert another row with different values
$name = 'two';
$value = 2;
$stmt->execute();
清单 5. 使用预处理语句取数据
$stmt = $dbh->prepare("SELECT * FROM REGISTRY where name = ?");
if ($stmt->execute(array('one'))) {
while ($row = $stmt->fetch()) {
print_r($row);
}
}
如果数据库驱动程序支持,您还可以绑定输出和输入参数。输出参数通常用于从存储过程获取值。输出参数使用起来比输入参数要复杂一些,当绑定一个给定的输出参数时,必须知道该参数的长度。如果为参数绑定的值大于您建议的长度,那么就会产生错误。
清单 6. 带输出参数调用存储过程
$stmt = $dbh->prepare("CALL sp_returns_string(?)");
$stmt->bindParam(1, $return_value, PDO_PARAM_STR, 4000);
// call the stored procedure
$stmt->execute();
print "procedure returned $return_value\n";
您还可以指定同时具有输入和输出值的参数,其语法类似于输出参数。在接下来的例子中,字符串 'hello' 被传递给存储过程,当存储过程返回时,hello 被替换为该存储过程返回的值。
清单 7. 带输入/输出参数调用存储过程
$stmt = $dbh->prepare("CALL sp_takes_string_returns_string(?)");
$value = 'hello';
$stmt->bindParam(1, $value, PDO_PARAM_STR|PDO_PARAM_INPUT_OUTPUT, 4000);
// call the stored procedure
$stmt->execute();
print "procedure returned $value\n";
错误和错误处理
PDO 提供了 3 种不同的错误处理模式,以满足不同风格的编程:
PDO_ERRMODE_SILENT
这是默认模式。PDO 将只设置错误代码,以通过 errorCode() 和 errorInfo() 方法对语句和数据库对象进行检查。如果错误是由于对语句对象的调用而产生的,那么可以在那个对象上调用 errorCode() 或 errorInfo() 方法。如果错误是由于调用数据库对象而产生的,那么可以在那个数据库对象上调用上述两个方法。
PDO_ERRMODE_WARNING
除了设置错误代码以外,PDO 还将发出一条传统的 E_WARNING 消息。如果您只是想看看发生了什么问题,而无意中断应用程序的流程,那么在调试/测试当中这种设置很有用。
PDO_ERRMODE_EXCEPTION
除了设置错误代码以外,PDO 还将抛出一个 PDOException,并设置其属性,以反映错误代码和错误信息。这种设置在调试当中也很有用,因为它会放大脚本中产生错误的地方,从而可以非常快速地指出代码中有问题的潜在区域(记住,如果异常导致脚本终止,则事务将自动回滚)。
异常模式另一个有用的地方是,与传统的 PHP 风格的警告相比,您可以更清晰地构造自己的错误处理,而且,比起以静寂方式以及显式地检查每个数据库调用的返回值,异常模式需要的代码/嵌套也更少。
PDO 定制了使用 SQL-92 SQLSTATE 错误代码字符串的标准;不同 PDO 驱动程序负责将它们本地代码映射为适当的 SQLSTATE 代码。例如,SQLSTATE 是用于 DB2(以及通常的 ODBC)的本地错误代码格式,这是多么方便啊!errorCode() 方法返回一个 SQLSTATE 代码。如果您需要关于一个错误的更多特定的信息,PDO 还提供了一个 errorInfo() 方法,该方法将返回一个数组,其中包含 SQLSTATE 代码、特定于驱动程序的错误代码以及特定于驱动程序的错误字符串。
分页数据、滚动游标和定位更新
在 Web 应用程序中,一种常见的范例是对查询结果进行分页。如果您使用一个 Internet 搜索引擎,那么很可能每天都会做这样的事。您输入搜索词,然后得到前 10-20 个匹配项。如果您想看到更多搜索结果,可以单击 "next page" 链接。如果想回头看前面看过的结果,可以单击 "previous page" 链接。记得在几年前,当我第一次在 Web 上使用这样的东西时,我对自己说:“为什么我不能通过滚动查看所有数据呢?” 问题的答案说简单也简单,说复杂也复杂 —— 我只想说,HTTP 不会智能地使数据库上的可滚动游标一直处于开放状态,即便如此,需要大量传输的 Web 应用程序也会很快地消耗掉大量开放的可滚动游标。因此,最简单的解决方案是为用户显示所有的匹配项 —— 但是用户很容易迷失在大量的结果当中。比较符合逻辑的措施是人工地将数据格式化到多个页面上,使用户可以每次查看一部分可以管理的数据。
所以人们编写可以取所有数据的 PHP 应用程序,然后只显示前 10 行。根据下一次请求,应用程序又显示 11-20 行,依此类推。这对于只返回少量数据的查询来说很不错,但是,如果有很多匹配项(比如多于 100),那么先取全部数据然后丢弃其中的 90%,这种做法很浪费。PHP 的创始人 Rasmus Lerdorf 就这种情形特地为 MySQL 发明了一个特殊的 "LIMIT, OFFSET" 子句。它允许您通知数据库,您只对一小部分行感兴趣,这样它就不会取其他不需要的行了。其语法(或非常类似的东西)已经被其他流行的开放源代码数据库采纳,但并不是所有数据库都提供了相同的语法。 Troels Arvin 收集了一些非常有用的信息,对不同 RDBMS 所支持的语法进行了比较。
如果您想在以 DB2 为后台数据库的 PHP 应用程序中实现分页结果,那么可以(也应该)使用下面示例中的语法。这里我们假设有一个 books 表,表中包含书名和作者,我们现在想要每次在一页中显示 10 个以上结果:
清单 8. 使用 SQL Standard "Window Functions" 实现数据分页
$db = new PDO('odbc:SAMPLE', 'db2inst1', 'ibmdb2');
// the offset is passed in from the user when they click on a link
// this cast to integer ensures that no SQL injection can occur
$offset = (int)$_GET['offset'];
$stmt = $db->prepare("select * from (
select
ROW_NUMBER() OVER (ORDER BY author) as rownum,
*
from books
) as books_window
WHERE rownum > $offset AND rownum <= (10 + $offset)");
if ($stmt->execute()) {
while (($row = $stmt->fetch()) !== false) {
print_r($row);
}
}
Cloudscape 说明
在撰写本文之际,Cloudscape 在其 SQL 实现中还不支持 ROW_NUMBER(),所以需要使用可滚动游标。
现在,如果您要编写一个更通用的应用程序,并希望实现分页的结果集,但是不想专门编写很多的代码,并且也不想使用更重量级的抽象层,Troels Arvin 的非常有帮助的 RDBMS 信息建议,您可以使用游标作为更轻便(稍微慢一点)的方案。碰巧的是,PDO 具有这方面的 API 级的支持。下面将谈到如何使用这种支持来达到与上面示例相同的效果:
清单 9. 使用滚动游标实现数据分页
$db = new PDO('odbc:SAMPLE', 'db2inst1', 'ibmdb2');
$stmt = $db->prepare("select * from books order by author", array(
PDO_ATTR_CURSOR => PDO_CURSOR_SCROLL));
// the offset is passed in from the user when they click on a link
// this cast to integer ensures that no SQL injection can occur
$offset = (int)$_GET['offset'];
if ($stmt->execute()) {
// moves the cursor to the requested offset and fetches the first
for ($tofetch = 10,
$row = $stmt->fetch(PDO_FETCH_ASSOC, PDO_FETCH_ORI_REL, $offset);
$row !== false && $tofetch-- > 0;
$row = $stmt->fetch(PDO_FETCH_ASSOC)) {
print_r($row);
}
}
需要强调的是,虽然滚动游标对于更冗长的 window 函数方案来说是一个很方便的替代方案,但这种方案要慢很多。如果在一个传输量比较少的环境中进行测试,您可能发现不了速度上的差异,但当规模扩大时,您就会开始发现速度降慢带来的痛苦。
定位更新
可滚动游标的另一个用途是,基于 SQL 中无法表达的重大标准驱动更新。如果您有一个 Web 页面链接的表,并且需要在每晚的批处理过程中更新那个表,以反映 Web 页面当前大小,那么可以编写如下代码:
清单 10. 使用滚动游标作出定位更新
$db = new PDO('odbc:SAMPLE', 'db2inst1', 'ibmdb2');
// create a named, scrolling, updateable cursor
$stmt = $db->prepare("select url, size from links FOR UPDATE OF size", array(
PDO_ATTR_CURSOR => PDO_CURSOR_SCROLL,
PDO_ATTR_CURSOR_NAME => 'link_pos'));
if ($stmt->execute()) {
// a statement for applying our updates.
// Notice the WHERE CURRENT OF clause mentions "link_pos",
// which is the name of the cursor we're using to select the data
$upd = $db->prepare("UPDATE links set size = ? WHERE CURRENT OF link_pos");
// grab each row
while (($row = $stmt->fetch()) !== false) {
// There are much more efficient ways to do this;
// this is a brief example only: grab all the content
// from the URL
$content = file_get_conents($row['url']);
// and measure its length
$size = strlen($content)
// and pass that as a parameter to our update statement
$upd->execute(array($size));
}
}
大型对象
在应用程序中的某个地方,您可能发现需要在数据库中存储“大型(large)”数据。大型通常意味着“大约 4kb 或 4kb 以上”,尽管在没有“大型”数据之前 DB2 最大可以处理 32kb 的数据。 大型对象可以是文本的,也可以是二进制的。PDO 允许在 bindParam() 或 bindColumn() 调用中通过使用 PDO_PARAM_LOB 类型代码来使用大型数据类型。PDO_PARAM_LOB 告诉 PDO 将数据映射为流,所以可以使用 PHP Streams API 来操纵这样的数据。下面是一个示例:
清单 11. 从数据库取一副图像
$db = new PDO('odbc:SAMPLE', 'db2inst1', 'ibmdb2');
$stmt = $db->prepare("select contenttype, imagedata from images where id=?");
$stmt->execute(array($_GET['id']));
list($type, $lob) = $stmt->fetch();
header("Content-Type: $type");
fpassthru($lob);
上面的介绍很简明扼要。现在让我们试试另一面,将上传的图像插入到一个数据库中:
清单 12. 将图像插入数据库中
$db = new PDO('odbc:SAMPLE', 'db2inst1', 'ibmdb2');
$stmt = $db->prepare("insert into images (id, contenttype, imagedata) values (?, ?, ?)");
$id = get_new_id(); // some function to allocate a new ID
// assume that we are running as part of a file upload form
// You can find more information in the PHP documentation
$fp = fopen($_FILES['file']['tmp_name'], 'rb');
$stmt->bindParam(1, $id);
$stmt->bindParam(2, $_FILES['file']['type']);
$stmt->bindParam(3, $fp, PDO_PARAM_LOB);
$stmt->execute();
这两个例子都是宏观层次的。请记住,被取的大型对象是一个流,可以通过所有常规的流函数来使用它,例如 fgets()、fread()、fgetcsv() 和 stream_get_contents()。