Oracle PHP 故障诊断常见问题解答

Christopher Jones
2004 年 1 月

Oracle 和 PHP 故障诊断提示和技巧。关于一般的 Oracle/PHP 常见问题解答,请参见由 Frank Naudé 提供的 Oracle/PHP 常见问题解答

主题


Oracle 没有安装或没有找到

如果在 PHP 中启用了 Oracle 支持,但未能找到 Oracle 客户端资料库,那么当您试图启动 Apache 时,将得到一个错误。例如,在 Windows 上,如果 php.ini 有 "extension=php_oci8.dll",但未能找到 Oracle 主页,则一条警告 "The dynamic link library OCI.dll could not be found in the specified path ...." 将被显示出来。

在启动 Apache 之前,确保 Oracle 环境变量被正确设置(参见下一主题)。此外,请参考由 Rob Clevenger 提供的 OTN 文章" 在 Windows 2000/XP 上安装 Oracle、PHP 和 Apache"。

Linux 用户可能看到一个关于不能加载 libclntsh.so 的 Apache 错误,但更可能在之前编译 PHP 时注意到这个问题。编译器将以一个错误 'Cannot find file "ocidfn.h"' 或 'Cannot find file "oci.h"' 而失败。

确保 Oracle 目录对编译 PHP 的 OS 用户是可读的。如果您安装了 Oracle,但丢失了 Oracle 头文件,请执行一次 Oracle 的 "Client" 安装。"Client" 安装指的是 Oracle9i Database Release 安装器中那个名称的选项。(给出的其它三个选项是 Database、Management and Integration、和 Cluster Management)。此外,还请参考 OTN 文章"在 Linux 上安装 Oracle、PHP 和 Apache"。


在启动 Apache 之前,在 shell 或环境中设置所有的 Oracle 环境变量。

在 Apache 启动之前设置所有的 Oracle 环境变量是使 PHP 能够与 Oracle 通信的一种安全的方法。在 PHP 脚本或 httpd.conf 文件中设置变量通常不起作用。由于通常对环境的混淆,存在许多邮件列表和论坛帖子。在 Windows 和 Linux 上的行为也不同。

OTN 文章" 在 Linux 上安装 Oracle、PHP 和 Apache "给出了一个名为 "start_apache" 的、用来设置环境和启动 Apache 的例子:

     #!/bin/sh
     ORACLE_HOME=/u01/app/oracle/product/9.2

     ORACLE_SID=orcl
     export ORACLE_HOME ORACLE_SID
     echo "Oracle Home: $ORACLE_HOME"
     echo "Oracle SID:  $ORACLE_SID"
     echo Starting Apache
     ./apachectl start

我做了一些测试来查看该环境对 PHP 调用有什么影响:

     $mycon = OCILogon("myusername", "mypassword", "MYDB");

我用了 RedHat Linux AS 2.1、Apache 1.3 和 PHP 4.3.3。

  1. 在 "apachectl start" 之前没有设置 ORACLE_HOME 并且在 PHP 脚本中没有 "putenv('ORACLE_HOME=/usr/oracle/MYDB')" 的情况下,我得到:

    Warning:ocilogon():_oci_open_server:
    Error while trying to retrieve text for error ORA-12154

    这显示连接失败,未能找到消息文件(这些文件位于 Oracle 主目录下)。

  2. 在 PHP 脚本中有 "putenv('ORACLE_HOME=/usr/oracle/MYDB')",但没有设置 ORACLE_HOME 的情况下,我得到:

    Warning:ocilogon():_oci_open_server: 
    ORA-12154:TNS:could not resolve service name

    连接仍然没有成功,但在错误出现之后能够从消息文件中读出消息正文。

  3. 在 "apachectl start" 之前设置了 ORACLE_HOME 但没有 "putenv()" 的情况下,连接成功了。这是推荐的配置。

  4. 在 "apachectl start" 之前正确设置 ORACLE_HOME 同时 "putenv()" 使用一个无效 ORACLE_HOME 目录的情况下,连接成功。我也在 Windows 上试验了这种情况。这次,连接失败了,出现和我上面的第一次测试同样的消息。

当我用一条 Apache httpd.conf 指令 "setenv ORACLE_HOME /usr/oracle/MYDB" 来替换 PHP putenv() 调用时,得到了一组类似的结果。

一些变量可以在 PHP 脚本中设置。在启动 Apache 之前正确设置 ORACLE_HOME 之后,以下操作改变了我的默认连接,并连接到 MYDB:

putenv("TWO_TASK=MYDB");
$mycon = OCILogon("myusername", "mypassword");

环境变量 TNS_ADMIN、NLS_DATE_FORMAT(可能还有其它变量)也可以用这种方式来设置。


与数据库连接

用户选择的网络服务名称常常用来识别要与哪个数据库连接。它被默认从环境变量 ORACLE_SID 中读出,或者它可以在连接调用中显式地给出。网络服务名称 MYDB 可以用在 Oracle 的命令行 SQL*Plus 实用程序中,如:

sqlplus myusername/mypassword@MYDB

或用在 PHP 中,如:

$mycon = OCILogon("myusername", "mypassword", "MYDB");

网络服务名称通常通过 tnsnames.ora 文件中的一个项目映射到一个实际的数据库上:

     MYDB =
       (DESCRIPTION =
          (ADDRESS_LIST =
           (ADDRESS = (PROTOCOL = TCP)(HOST = mymachine.mydomain)(PORT = 1535))
) (CONNECT_DATA = (SERVER = DEDICATED) (SERVICE_NAME = MYDB.mydomain) ) )

$ORACLE_HOME/network/admin/tnsnames.ora 文件被默认使用。在一些操作系统上,如果默认文件不存在,则将检查其它的位置。

如果在 tnsnames.ora 中找不到被用在 OCILogon() 中的网络服务名称,或者 PHP 根本没找到 tnsnames.ora,那么您在登录时可能得到一个错误:

Warning:ocilogon():_oci_open_server:ORA-12154:TNS:could not resolve service name

在启动 Apache web server 之前,检查环境变量 ORACLE_HOME 是否正确设置(参见之前的主题)。

如果您 tnsnames.ora 在一个非默认的位置,您可以将环境变量 TNS_ADMIN 设置为包含它的目录。例如,如果您在使用 /tmp/tnsnames.ora,将这些行添加到 start_apache 中(同样,参见之前的主题):

TNS_ADMIN=/tmp
export TNS_ADMIN

另一种解决办法是在 OCILogon() 调用中使用完整的连接字符串:

      $db = "(DESCRIPTION=(ADDRESS_LIST=(ADDRESS=(PROTOCOL=TCP)
                   (HOST = mymachine.mydomain)(PORT=1535)))
                   (CONNECT_DATA=(SERVER=DEDICATED)
                   (SERVICE_NAME=MYDB.mydomain)))";

      $mycon = OCILogon("myusername", "mypassword", $db);
      ...

如果 $ORACLE_HOME/network/admin/sqlnet.ora 与 tnsnames.ora 不同步且一个域名被隐式地添加到了别名中,那么错误 ORA- 12154 也可能发生。OCILogon() 调用中的不合格网络服务名称将添加 sqlnet.ora 的 NAMES.DEFAULT_DOMAIN 值。例如,如果 sqlnet.ora 有:

NAMES.DEFAULT_DOMAIN = au.oracle.com

那么 'OCILogon("myusername", "mypassword", "mydb")' 将使 Oracle 在 tnsnames.ora 中寻找别名 "MYDB.AU.ORACLE.COM = ..."。一种快速的解决办法是将 tnsnames.ora 项目改变为:

        MYDB.AU.ORACLE.COM =
          (DESCRIPTION =
            (ADDRESS_LIST =
              (ADDRESS = (PROTOCOL = TCP)(HOST = mymachine.mydomain)(PORT = 1535))
            )
            (CONNECT_DATA =
              (SERVER = DEDICATED)
              (SERVICE_NAME = MYDB.mydomain)
            )
          )

回显 SQL 语句,检查它们是否得到正确设计

令人意外的是,没有得到结果或者得到错误的结果常常归因于执行了错误的语句。在开发期间,从 PHP 回显每一条完整的 SQL 语句,检查它是否得到正确设计、所有变量是否得到了正确扩展。引用错误或者对字符串中 PHP 的变量语法的错误理解可能导致不正确的语句被执行。

在执行它们之前测试 SQL*Plus 中的 SQL 语句也有助于确认正确性。

当语句被输入到诸如 SQL*Plus 之类的工具中时,通常用一个分号来告诉工具这个语句完成了,现在可以被执行。不过,分号不被认为是语句的一部分,并且不会被发送到数据库中。在 PHP 中,不要向 SQL 语句中添加分号,否则将出现一个 Oracle 错误。这个例子是一次有效的查询:


$sql = "SELECT * FROM EMP_DETAILS_VIEW";

Oracle 内置的脚本语言 PL/SQL 具有与 SQL 不同的语法,在句尾需要一个分号:

$plsql = "BEGIN DBMS_OUTPUT.PUT_LINE('hi'); END;";

始终测试来自数据库函数调用的返回代码

测试来自 OCI 函数的所有返回值,防止隐藏的问题和误导性的结果。

为了进行基本的测试,将 php.ini 的 error_reporting 指令设置为 E_ALL。 此外,设定 display_errors,或配置错误日志文件(记得检查它!)。这将在 Oracle 错误出现时显示它们,除非您已经显式地为 Oracle 函数调用添加了前缀 "@"


插入包含引号的字串

插入包含单引号的字符串可以用几种方式来处理:

  • 使用赋值变量。这还为 "SQL Injection" 安全性问题提供了保护:

            $name = "O'Reilly";
            $stmt = 'INSERT INTO CUSTOMERS (NAME) VALUES (:nm)';
    $stid = OCIParse($mycon, $stmt); OCIBindByName($stid, ':nm', $name, -1); OCIExecute($stid);
  • 使所有的单引号变为双引号:

            $name = "O'Reilly";
            $name = str_replace("'", "''", $name);
            $stmt = "INSERT INTO CUSTOMERS (NAME) VALUES ('".$name."')";
  • 打开 php.ini 中的 magic_quotes_sybase,然后使用 addslashes()

    $name = addSlashes("O'Reilly");
    $stmt = "INSERT INTO CUSTOMERS (NAME) VALUES ('".$name."')";

    由于可移植性问题,不推荐这么做。


在 PHP 中使用 Oracle 赋值变量

绑定问题常常由于需要在调用 OCIExecute() 时(而不只是在 OCIBindByName() 被执行时)访问数值而引起。

如果 OCIBindByName() 在一个包装函数或方法内部被调用,而传递给 OCIBindByName() 的 PHP 变量对该包装函数而言是本地的,那么就可能存在问题。当 OCIExecute() 稍后被调用时,变量需要在有效范围中。否则可能出现一个诸如 "OCIStmtExecute:ORA-01460:unimplemented or unreasonable conversion requested" 之类的 Oracle 错误;或者,它可能看似没有为 "OUT" 变量设定值,令人迷惑。

下面的示例是上述情况的一个变样。变量 "$val" 对 "foreach" 命令而言是本地的。代码没有返回记录:

      $dn, ':loc' => $lc);

     $conn = OCILogon('myusername', 'mypassword', 'mydb');
     $stmt = OCIParse($conn, $qs);

     foreach ($ba as $key => $val)
     {
       OCIBindByName($stmt, $key, $val, -1);
     }

     OCIExecute($stmt);

     while ($succ = OCIFetchInto($stmt, $o)) {
       foreach ($o as $mv) {
         echo $mv." ";
       }
       echo "
\n"; } ?>

修改 OCIBindByName() 调用可以解决这个问题:

     ...
     foreach ($ba as $key => $val)
     {
       OCIBindByName($stmt, $key, $ba[$key], -1);
     }
     ...

上载 LOBS

OCINewDescriptor() 人工输入中给出了上载 BLOB 的一些示例代码。下面是加载 CLOB 的一个最新示例。

小心 HTML 脚本中的大小限制:


或者 httpd.conf 中 LimitRequestBody 指令中的大小限制(参考 PHP Bug 22138):

一个上载到 CLOB 中的示例是:

     
     // using PHP's Oracle 8 API.
     //
     // Based on http://www.php.net/manual/en/function.ocinewdescriptor.php
// modified to work on CLOBs and use register_globals = Off. // // Before running this script, execute these statements in SQL*Plus:
// drop table myclobtab; // create table myclobtab (c1 number, c2 clob); // // Tested with PHP 4.3.3 against Oracle 9.2 // if (!isset($_FILES['lob_upload'])) { ?>
Upload file:
$conn = OCILogon('myusername', 'mypassword', 'mydb'); // Delete any existing CLOB so the query at the bottom // displays the new data $query = 'DELETE FROM MYCLOBTAB'; $stmt = OCIParse ($conn, $query); OCIExecute($stmt, OCI_COMMIT_ON_SUCCESS); OCIFreeStatement($stmt); // Insert the CLOB from PHP's tempory upload area $lob = OCINewDescriptor($conn, OCI_D_LOB); $stmt = OCIParse($conn, 'INSERT INTO MYCLOBTAB (C1, C2) VALUES('.
$myid . ', EMPTY_CLOB()) RETURNING C2 INTO :C2');
OCIBindByName($stmt, ':C2', $lob, -1, OCI_B_CLOB); OCIExecute($stmt, OCI_DEFAULT); // The function $lob->savefile(...) reads from the uploaded file. // If the data was already in a PHP variable $myv, the // $lob->save($myv) function could be used instead. if ($lob->savefile($_FILES['lob_upload']['tmp_name'])) { OCICommit($conn); echo "Clob successfully uploaded\n"; } else { echo "Could not upload Clob\n"; } $lob->free(); OCIFreeStatement($stmt); // Now query the uploaded CLOB and display it $query = 'SELECT C2 FROM MYCLOBTAB WHERE C1 = '.$myid; $stmt = OCIParse ($conn, $query); OCIExecute($stmt, OCI_DEFAULT); OCIFetchInto($stmt, $arr, OCI_ASSOC); $result = $arr['C2']->load(); echo '
';
       echo $result;
       echo '
'; OCIFreeStatement($stmt); OCILogoff($conn); } ?>

PHP 中的数组获取

在 PHP 中没有数组获取功能,但用 OCISetPrefetch() 来预先获取行数是类似的,并且可以显著地提高性能。设置预取行数而不是数组大小的好处是 Oracle 将为您执行高速缓存。您的数据结构一次只需处理一行,而且您的代码通常能够更简单。

设置预取行数为 100 的示例如:

     \n";
     }

     OCILogoff($conn);

     ?>

PEAR DB 中的 Oracle 错误消息

PEAR DB 接口是一个数据库抽象层,它对不同品牌的数据库使用同样的语法。标准的 PEAR DB 错误函数 $db->getMessage() 返回一条简单的 PEAR 错误说明。例如,如果由于任何原因连接失败,消息始终为:

     DB Error: connect failed

通过使用以下方法,您可以获得准确的 Oracle 错误号和消息:

      $db->getDebugInfo()

它包括 Oracle 错误和完整的语句,如:

[nativecode=ORA-01017:invalid username/password; logon denied ] 
		** oci8://myusername:wrongpassword@mydb

利用一个包装函数,可以从这个字符串中提取出 Oracle 消息正文。例如:

     require_once('DB.php');

     $db = DB::connect("oci8://myusername:mypassword@mydb");
     if (DB::iserror($db)) {
       PrintPEARDBError($db);
       die();
     }

     .
     .
     .

     // Display PEAR DB error
     function PrintPEARDBError($e)
     {
       if (is_object($e)) {
         $s = preg_match('/.*\[nativecode=(.*)/', $e->getDebugInfo(), $r);
         $etxt = $s ? $r[1] : $e->getDebugInfo();
       }
       else {
         $etxt = "Unknown Error";
       }
       echo "

Error:

\n
		   ".htmlspecialchars($etxt)."
\n"; }

来自 PrintPEARDBError() 的输出是:

ERROR:

     ORA-01017: invalid username/password; logon denied


OCI 线程安全

PHP 库在一个 bug — 这个 bug 在 4.3.4 版(在编写时这是最新的版本)发布之后得到了修复 — 中具有 OCI 程安全性。在实践中,似乎只有加载的服务器遇到了问题,这些问题有随机的行为(包括崩溃)。补丁在最新的快照中提供。参见 PHP bug 26558PHP Bug 26393


使用 AS SYSDBA 或 AS SYSOPER 进行连接

在 PHP 中连接 AS SYSDBA 或 AS SYSOPER 是不可能的。

PHP 的 oci8.c 中对 Oracle 的 OCISessionBegin() 函数的调用始终传递 OCI_DEFAULT。为了允许授权的连接,这将需要更改为 OCI_SYSDBA 或 OCI_SYSOPER。但这个示例解决方案打开了一个潜在的安全漏洞,参见 Re:关于 php ocilogon() oracle OCI FUNCTION 的建议


PHP 中的 NCHAR 和 NCLOB 支持

在 PHP 中没有 NCHAR 或 NCLOB 支持。

当目前 oci8.c 的 PHP 实施调用 Oracle 的 OCI 时,它始终将 SQLCS_IMPLICIT 用于字符集形式。字符集形式需要为 SQLCS_NCHAR,以支持 NCHAR 和 NLCOB,而且在 PHP 代码中可能需要其它的数据处理变化。

寄送此页面
Printer View 打印机视图