一名新 PHP 数据对象 (PDO) 数据抽象层的原始开发人员为您简要介绍该抽象层,重点讲述与 Oracle 一起运行的情况。
需要 PHP:5.0
需要其他:Oracle 8 或更高版本客户端库
下载用于 Oracle 的 PDO (Windows):php_pdo.dll, php_pdo_oci.dll
下载用于 Oracle 的 PDO (Unix):pdo, pdo_oci
% ./configure --prefix=/usr/local/php5 --with-zlib [此处指定其他选项] % make install
% PATH="/usr/local/php5/bin:$PATH" % pear install PDO-alpha
% wget http://pecl.php.net/get/PDO % pear install PDO-0.1.1.tgz
% wget http://pecl.php.net/get/PDO_OCI % pear install PDO_OCI-0.1.tgz
<?php try { $dbh = new PDO("OCI:", "scott", "tiger"); } catch (PDOException $e) { echo "Failed to obtain database handle " .$e->getMessage(); } ?>
<?php try { $dbh = new PDO("OCI:dbname=accounts;charset=UTF-8", "scott", "tiger"); } catch (PDOException $e) { echo "Failed to obtain database handle " .$e->getMessage(); } ?>
省略 try..catch 控制结构并无裨益。如果在应用程序的较高级别没有定义异常处理,则在无法建立数据?/
目前,PDO 完全没有执行自己的任何连接管理,因此每个“新 PDO”调用都会建立一个新的数据库连接。该连接在 $dbh 变量越界时,或者当您为其指定 NULL 值时会被释放。
<?php try { $dbh = new PDO("OCI:dbname=accounts;charset=UTF-8", "scott", "tiger"); } catch (PDOException $e) { echo "Failed to obtain database handle " .$e->getMessage(); exit; } // 在此处对数据库执行一些操作 // ... // 现在完成,释放该连接 $dbh = null; ?>
计划在不久的将来为 PDO 增加连接缓存功能;就当前的 oci8 扩展而言,会重用与现有服务器的连接,并且在这些连接中,还会重用闲置的登录。当在缓存连接模式中运行时,如上面的代码段所示释放 $dbh 时会将该登录标记为可由其他连接重用。
如果您使用 ODBC 驱动程序访问 Oracle,则可能会很高兴地注意到,默认情况下 PDO_ODBC 驱动程序支持 ODBC 连接池。
了解一个编程 API 的最好方式就是使用它,因此我们来看一下附带的这个演示,以了解如何进行批次更新(代码如下)。
<?php // Create a PDO database handle object // the 'oci:' string specifies that the OCI driver should be used // you could use 'oci:dbname=name' to specify the database name. // The second and third parameters are the username and password respectively $dbh = new PDO('oci:', 'scott', 'tiger'); // Create a test table to hold the data from credits.csv $dbh->exec(" CREATE TABLE CREDITS ( extension varchar(255), name varchar(255) )"); // start a transaction $dbh->beginTransaction(); // prepare to insert a large quantitiy of data $stmt = $dbh->prepare("INSERT INTO CREDITS (extension, name) VALUES (:extension, :name)"); // bind the inputs to php variables; specify that the data will be strings // with a maximum length of 64 characters $stmt->bindParam(':extension', $extension, PDO_PARAM_STR, 64); $stmt->bindParam(':name', $name, PDO_PARAM_STR, 64); // Open the .csv file for import $fp = fopen('credits.csv', 'r'); while (!feof($fp)) { list($extension, $name) = fgetcsv($fp, 1024); $stmt->execute(); } fclose($fp); // Commit the changes $dbh->commit(); ?>
既然我们已经成功连接到了 Oracle,那么现在就可以创建一个表来保存一些数据了。对于此示例,我们使用一些 PHP 扩展及其作者,并将这些内容输入一个数据库中。数据库句柄对象的 exec() 方法可用来发出不会返回结果集的快速一次性查询,因此我们在这里使用该方法来发出 CREATE TABLE 查询。
为了使得示例更自然,我从 PHP 源代码中抽取了扩展及其作者的信息,并将其存储到了一个 CSV 文件中(请参见“相关附件:credits.csv”)。这就代表一个常见情形:从 CSV 文件批次导入数据。在我们的示例中,我们充分利用了 Oracle 的预处理语句和绑定参数,以获得一个高效的数据导入脚本。在讲述该示例之前,有必要了解一下 PDO 处理事务的方式。
Oracle 具有一个敏感的默认操作模式:当您进行连接时,将会位于一个隐式事务处理中,在提交事务之前其中的更改不会完全生效。除了事务处理的标准优点(原子性、一致性、隔离性、可持久性 - ACID)之外,数据库服务器在执行每次更新之后还不需要重新构建索引和其他内部结构;它可以延迟到提交之后进行。这样会加速代码的执行。Oracle 这点确实很好。
但不幸的是,并非每个数据库供应商都支持事务处理,并且因为 PDO 旨在以一种相对可移植的方式支持这些事务处理,所以它默认情况下以自动提交模式运行。启用自动提交模式后,数据库驱动程序会隐式提交每个成功的更新。当您调用 $dbh->beginTransaction() 时,就会请求关闭自动提交,直到调用 $dbh->commit() 或者 $dbh->rollBack() 才会重新启用,具体取决于您的代码是怎样编写的。如果基础驱动程序不支持事务处理,则会抛出一个 PDOException。
如果发生了问题并且 PHP 出错,您的脚本将退出并且事务处于待批状态;或者您关闭数据库句柄时,PDO 会自动针对任何待批的事务调用 $dbh->rollBack()。此行为会减少向数据库中提交可能未定义或者已损坏数据的可能性,这是用于处理已放弃事务的标准语义。
PDO 支持使用 Oracle 样式命名的占位符语法将变量帮定到 SQL 中的预处理语句(与 oci8 扩展中的 ocibindbyname() 类似)。PDO 还为其他数据库(如 ODBC)提供了命名占位符模拟,甚至可以为生来就不支持该概念的数据库(如 MySQL)模拟预处理语句和绑定参数。这是 PHP 向前迈进的积极一步,因为这样可以使开发人员能够用 PHP 编写“企业级”的数据库应用程序,而不必特别关注数据库平台的能力。
使用 PDO 预处理语句非常简单,调用数据库句柄的 prepare() 方法即可。它会返回一个语句句柄对象,然后您可以使用该对象来绑定参数和执行语句。在此示例中,我们将要定义两个命名占位符,“:extension”和“:name”,这两个占位符分别与 .CSV 文件中的 PHP 扩展名称和其中一个作者的姓名相对应。
$stmt = $dbh->prepare("INSERT INTO CREDITS (extension, name) VALUES (:extension, :name)");
预处理了语句之后,我们使用 bindParam() 方法来将这些命名参数分别与 PHP 变量名称“$extension”和“$name”相关联(这与 ocibindbyname() 类似)。我们还会通知 Oracle,这些数据将要格式化为字符串,最大长度为 64 个字符。
$stmt->bindParam(':extension', $extension, PDO_PARAM_STR, 64); $stmt->bindParam(':name', $name, PDO_PARAM_STR, 64);
我们现在即准备好插入数据了 - 我们只需要打开该 CSV 文件,并从中获取数据即可。通过使用 fopen() 和 fgetcsv() 函数可以相当简单地完成此操作。然后,我们可以使用 PHP list() 构造函数直接将 CSV 的列指定给变量“$extension”和“$name”。因为这些变量已经绑定到了语句中,所以我们现在要做的只是调用该语句对象的 execute() 方法使其执行插入。这种方式既方便又快捷 - 在事务处理时每个迭代循环只有两行。到达文件尾时,我们就可以立即使用数据库句柄的 commit() 方法来提交这些更改了。
如果您只是要传递输入参数,并且有许多这样的参数要传递,那么您会觉得下面所示的快捷方式语法非常有帮助;此语法使您能够省去对 $stmt->bindParam() 的调用。
$stmt = $dbh->prepare("INSERT INTO CREDITS (extension, name) VALUES (:extension, :name)"); $stmt->execute(array(':extension' => $extension, ':name' => $name));
您还可以使用 bindParam 来为存储过程设置输入/输出参数;语法是完全相同的,只是查询有所不同。下面的代码演示如何调用一个名为“sp_add_item”的存储过程;其目的是要针对输入设置 $item_name,然后该存储过程将在返回时更新 $error_code。
$stmt = $dbh->prepare("begin sp_add_item(:item_name, :error_code); end"); $stmt->bindParam(':item_name', $item_name, PDO_PARAM_STR, 12); $stmt->bindParam(':error_code', $error_code, PDO_PARAM_STR, 12); $stmt->execute();
使用 PDO 抓取数据与进行插入或更新相似,只是您执行完查询之后,将要重复调用 fetch() 方法来获取结果集的下一行。进行获取的最简单情况如下所示,值得注意的一点是,您还可以将参数绑定到查询,以控制如 WHERE 子句这样的内容;执行此操作的语法与我们已经看到的 bindParam() 代码完全相同。
$stmt = $dbh->prepare("SELECT extension, name from CREDITS"); if ($stmt->execute()) { while ($row = stmt->fetch()) { print_r($row); } }
PDO 支持一些不同的抓取策略,这些策略在方便性和性能方面有所差别;通过将下列选项之一指定为 fetch() 方法的参数,您可以更改其返回值以适应您的语法:
·PDO_FETCH_NUM - 每个行抓取返回一个按照列位置索引的数组,并且以 0 为基数(第一列是第 0 个元素)。
while ($row = $stmt->fetch(PDO_FETCH_NUM)) { printf("Extension %s, by %s<br>", $row[0], $row[1]); }
·PDO_FETCH_ASSOC - 每个行抓取根据行集中的列名,返回一个按列名索引的数组。
while ($row = $stmt->fetch(PDO_FETCH_ASSOC)) { echo "Extension $row[EXTENSION] by $row[NAME]<br>"; }
·PDO_FETCH_BOTH - 每个行抓取返回一个既按照列位置又按照列名索引的数组。也就是上述两种情况的直接组合。如果没有指定抓取模式,则该模式为默认模式。
·PDO_FETCH_OBJ - 每个行抓取返回一个匿名对象,其属性名与列名对应。
while ($row = $stmt->fetch(PDO_FETCH_ASSOC)) { echo "Extension {$row->EXTENSION} by {$row->NAME}<br>"; }
·PDO_FETCH_LAZY - 每个行抓取返回一个引用语句对象的重载对象。这“看起来”好像是 PDO_FETCH_OBJ 和 PDO_FETCH_BOTH 的组合,只是只有当您在脚本中访问 PHP 变量时才创建这些变量。
·PDO_FETCH_BOUND - 抓取每行,返回 TRUE。在使用绑定输出列时这种方式非常有用,它可以避免创建不需要的任何数组或对象。(请参见下面的示例)。
无论您使用哪种抓取策略,当没有其他行可抓取时,fetch() 方法将会返回 FALSE。
现在我要讲述一些技巧,如果您需要最后再调整一下脚本性能的话,这些技巧可能会对您有所帮助。但先给你一个忠告:要像躲避瘟疫一样避免不成熟的优化。您应该总是首选最清晰、可维护性最好的解决方案。请记住,在一个典型的 Web 应用程序中,您不能衡量各种抓取模式间的区别,除非脚本要处理很多行。我再重复一遍:抓取模式间的性能区别非常小 - 请使用最适合您代码的模式。
请记住,使用 PDO_FETCH_NUM 的花销最小,因为访问列数据只是一个简单的数值查询。PDO_FETCH_OBJ 使您能够使用 OO 语法将数据集的列作为对象的属性来访问,但是每个属性访问都涉及一个附加的散列查询,使得使用它的花销基本上与 PDO_FETCH_ASSOC 相同。每个这样的模式都会复制整行,从而占用稍多的内存。
很多数据库驱动程序都会代表您预先抓取并缓存一定数量的行。PHP 每次访问其中一个这样行中的列时,它都需要将其复制到自己的专用内存区域中。如果您的查询涉及很多行,而只需要基于某种复杂的逻辑访问给定行的特定列,则您会发现 PDO_FETCH_LAZY 是一种避免使用很多内存的有用方法,因为它只有在您访问给定列时才复制该列。使用此方式时要注意,从某个给定语句为每个 fetch() 抓取的“惰性对象”是每次迭代时使用的同一对象(以减少每次创建/销毁它的开销)。这就暗示着您不能只是简单地存储该对象用于以后的比较,因为它仍然会引用该语句的当前行 - 您需要手动复制所需要的部分。
最后一种模式为 PDO_FETCH_BOUND,该模式会告知 PDO 您已经将所有列绑定到了 PHP 变量,并且除了要它在到达行集的末尾时通知您外不需要它执行别的任何操作。绑定输出列在概念上与绑定输入参数相似,只是绑定输出列可以用于所有数据库驱动程序。您可以将 PHP 变量绑定到命名列,PDO 将在每次调用 execute() 时对其进行更新。此技术可用来剃去结果集中每列、每行的一些虚拟机器操作码(这种代码速度比原生码要慢)。这种技术的缺点在于,可能会使您的代码难以跟踪(也称为 WTF 系数较高),您使用变量名称时需要倍加小心。下面的代码说明了绑定输出列的使用。请注意,您不必指定 PDO_FETCH_BOUND 即可使用 $stmt->bindColumn();PDO_FETCH_BOUND 只是一个对于您了解只能使用绑定值的情况的一种优化。
$stmt = $dbh->prepare("SELECT extension, name from CREDITS"); if ($stmt->execute()) { $stmt->bindColumn('EXTENSION', $extension); $stmt->bindColumn('NAME', $name); while ($stmt->fetch(PDO_FETCH_BOUND)) { echo "Extension:$extension, Author:$name\n"; } }
区分大小写的列
PDO 旨在令使用可移植 SQL 的脚本运行良好、可移植。本文中提及的所有查询(调用存储过程除外)在使用任何 PDO 驱动程序时其运行性能应该相同 - 包括所有绑定输入变量和绑定输出列。
但有一个转换问题 - 当您使用 PDO_FETCH_ASSOC 抓取数据时,不同的驱动程序会以不同的方式返回列名 - 某些会将列名转化为大写,某些转换为小写,某些则会使其呈查询中指定的样式。这对于 PHP 脚本来说是一个潜在的问题,因为数组键区分大小写。PDO 提供了一个兼容性属性来帮助规范脚本的结果。下面的小代码段是上面 PDO_FETCH_BOUND 示例的可移植版本,因为 setAttribute() 方法调用会指导 PDO 将抓取返回的列名全部转换为大写:
$dbh = new PDO('OCI:', 'scott', 'tiger'); $dbh->setAttribute(PDO_ATTR_CASE, PDO_CASE_UPPER); stmt = $dbh->prepare("SELECT extension, name from CREDITS"); if ($stmt->execute()) { $stmt->bindColumn('EXTENSION', $extension); $stmt->bindColumn('NAME', $name); while ($stmt->fetch(PDO_FETCH_BOUND)) { echo "Extension:$extension, Author:$name\n"; } }
除了 PDO_CASE_UPPER 之外,还有 PDO_CASE_LOWER(它会将列名转换为小写)和 PDO_CASE_NATURAL(它是默认选项:使列保持数据库驱动程序返回的形式)。
错误和错误处理
可移植脚本的另一个难题是处理从各种数据库处理程序返回的各种不同的错误消息;某些数据库对于程序化处理错误的支持能力很差,而其他一些数据库则具有非常丰富的错误代码。只要可行,PDO 将为您的脚本提供一个统一的错误代码,从而使您不必为应对可移植性的这个方面所累。当然,PDO 还会为驱动程序提供原生错误代码和错误消息,以防您需要用它来进行诊断,或者错误代码映射不完整。
另一个困扰 PHP 数据库扩展的一致性问题是错误处理策略的一致性:某些扩展会返回的错误代码需要您手动抓取错误字符串,而其他一些扩展则只是发出 PHP 警告。PDO 允许您从下列三种不同的错误处理策略中选择一种:
·PDO_ERRMODE_SILENT
这是默认模式;它只是使用语句和数据库句柄对象的 errorCode() 和 errorInfo() 方法为您设置要检查的错误代码。
if (!$dbh->exec($sql)) { echo $dbh->errorCode() ."<BR>"; $info = $dbh->errorInfo(); // $info[0] == $dbh->errorCode() 统一的错误代码 // $info[1] 是驱动程序特定的错误代码 // $info[2] 是驱动程序特定的错误字符串 }
·
PDO_ERRMODE_WARNING
除了设置错误代码之外,PDO 还会发出 PHP 警告,您可以使用常规的 PHP 错误处理程序捕获该警告,并集中应用您准备好用于应用程序的任何错误处理/记录策略,或者只是使该错误显示在浏览器中(在内部测试过程中非常有用)。
·
PDO_ERRMODE_EXCEPTION
除了设置错误代码之外,PDO 还会抛出一个 PDOException,并将其属性设置为包含该错误代码和信息。然后,您可以在代码的较高级别捕获该异常,使用全局异常处理程序捕获该异常,或者不对其进行处理而终止脚本(此时将回滚任何未决的事务)。
try { $dbh->exec($sql); } catch (PDOException $e) { // 显示警告消息 print $e->getMessage(); $info = $e->errorInfo; // $info[0] == $e->code; unified error code // $info[1] 是驱动程序特定的错误代码 // $info[2] 是驱动程序特定的错误字符串 }
请注意,与警告或异常相比,静默模式针对运行时错误使用的资源最少,但是为了获得该速度,您牺牲了一些简单性,而变得有一点复杂。
统一错误代码表当前包括下列常量: PDO_ERR_NONE、PDO_ERR_CANT_MAP、PDO_ERR_SYNTAX、PDO_ERR_CONSTRAINT、PDO_ERR_NOT_FOUND、PDO_ERR_ALREADY_EXISTS、PDO_ERR_NOT_IMPLEMENTED、PDO_ERR_MISMATCH、PDO_ERR_TRUNCATED、PDO_ERR_DISCONNECTED。
这些常量所代表的意思字面即可推知,但是 PDO_ERR_CANT_MAP 代码除外;这是一个 PDO 特定的代码,也就是说它无法将驱动程序特定的代码映射到统一的错误代码,因此您应该查询 errorInfo() 方法返回的驱动程序特定代码来获得更多信息。
数据类型
PDO 在某种程度上类型不可知,因此它喜欢将数据表示为字符串,而不是将其转换为整数或双精度类型。此时您可能对此有些迷惑,但是原因非常简单:字符串类型是最精确的类型,在 PHP 中具有最广泛的应用范围;过早地将数据转换为整数或者双精度类型可能会导致截断或舍入错误。通过将数据以字符串抽出,PDO 为您提供了一些脚本控制,您可以使用普通的 PHP 类型转换工具(如数学运算过程中的转换和隐式)来控制如何进行转换以及何时进行转换。
NULL
如果结果集中的某列包含一个 NULL 值,PDO 则会将其映射为 PHP null 值。Oracle 在将数据返回 PDO 时会将空字符串转换为 NULL,但是 PHP 支持的任何其他数据库都不会这样处理,从而导致了可移植性问题。PDO 提供了一个驱动程序级属性 PDO_ATTR_ORACLE_NULLS,该属性会为其他数据驱动程序模拟此行为:
$dbh = new PDO('OCI:', 'scott', 'tiger'); $dbh->setAttribute(PDO_ATTR_ORACLE_NULLS, true); // 现在从此 $dbh 打开的任何语句中的 // 空字符串都将被转换为 NULL
PDO 现在仍相当不成熟,但是会快速成熟起来。在编写本文之时,我在本文中提到的任何内容都能够通过 PDO_OCI 驱动程序适用于 Oracle 8 或更高版本(在 Oracle 8.0 和 9.2 上测试过)。
已经计划增加以下主要特性,在不久将可以使用:
1.使用 PHP 流的 LOB 支持。 使用绑定参数,您能够将任何流资源(如文件、套接字、HTTP 资源、压缩/筛选的流)作为输入或输出参数传递到在 LOB 上运行的查询中。与之相似,类型为 LOB 的输出参数将表现为 PHP 流,因此您可以使用 fread()、fwrite()、fseek() 和其他流函数来访问这些参数。此时,在 PDO 中根本没有 LOB 支持。
2.持久性连接和缓存的预处理语句。 持久性连接使您能够避免在每个页面命中时打开和关闭数据库服务器连接。缓存的预处理语句又前进了一步,它使您能够持久保持查询的预处理版本以及数据库句柄。
3.游标。 目前,PDO 只提供前向只读游标,但是将来会提供可滚动游标(需要基础驱动程序支持)、REF-CURSOR、使用游标进行定位更新,以及可更新滚动游标。
我们希望在 PHP 5.1 中默认启用 PHP 扩展(距此目标尚远),但是在此之前,我们希望能让 PDO 在 PHP 5.0 发布时稳定运行,但是我们日常工作中的压力稍稍拖延了这些工作。同时,通过 PECL 发布 PDO 使我们能够在收到问题报告时做出回应,并根据不同于 PHP 5.0 发布时间表的时间表发布修复版本,因此您在 PHP 5.1 发布前即可使用 PDO。
我们需要您的反馈
如果您试用了 PDO,并且发现了问题,请务必使用我们的错误跟踪软件将其报告给我们。如果您使用的是 Oracle 驱动程序,则请使用此页:
http://pecl.php.net/bugs/report.php?package=PDO_OCI
如果您使用的是其他驱动程序,则请用其名称替换该 URL 中 PDO_OCI。
如果您使用 PDO 时遇到问题,或者针对某些特性存在疑问,或者具有特性请求,请联系 pecl-dev@lists.php.net。如果您愿意,当然还可以直接联系我 (wez@php.net),但是请注意,我每天都会收到大量有关 PHP 的电子邮件;您可能会发现如果首先与前面的邮件列表联系会更快得到答复。
-----------
关于作者
Wez Furlong 是 Brain Room Ltd. 的技术总监,他在该公司不但使用 PHP 用于 Web 开发,还将其用作 Linux 和 Windows 应用程序和系统的嵌入式脚本引擎。Wez 是 PHP 的核心开发人员,经常向 SQLite、COM/.Net、ActivePHP、mailparse 和 Streams API 等投稿,他是 PECL 即 PHP 扩展社区库的“头儿”。他的咨询公司的网页为 http://www.thebrainroom.net。
资源
1.有关 PDO 程序包的更多信息
2.报告新错误? - 请提供可能有助于修复该错误的任何信息。
3.用于 Oracle JDeveloper 的 PHP 扩展 - 该 PHP 扩展简化了 Oracle JDeveloper 10g 中 PHP 脚本的创建、编辑和运行
4.PHP 漫游者指南 - 了解关于 PHP 的一切,从启动您的第一个应用程序到这种语言将拥有怎样的未来。