AnyCoアプリケーションのOracle Database機能は、すべてのPHP OCI8アクセスを処理する1つのクラスに抽象化します。
この章のトピックは、次のとおりです。
PHPファイルac_cred.inc.phpを作成します。'.inc'は慣習的に名前に含められるもので、このファイルに関数やクラスなどの定義しか含まれていないことを表しています。末尾に付いた拡張子.phpは、ユーザーがこのファイルに対してなんらかのHTTPリクエストを発行したときに、Webサーバーからユーザーにテキストが送信される(セキュリティ・リスク)のではなく、そのファイルがPHPスクリプトとして実行されることを意味しています。ファイルには定義しか含まれていないので、ユーザーに送信される出力はありません。これにより、望ましくない結果やコードの露呈を防止できます。
ac_cred.inc.phpファイルの初期の状態は次のようになります。
<?php
/**
* ac_cred.inc.php: Secret Connection Credentials for a database class
* @package Oracle
*/
/**
* DB user name
*/
define('SCHEMA', 'hr');
/**
* DB Password.
*
* Note: In practice keep database credentials out of directories
* accessible to the web server.
*/
define('PASSWORD', 'welcome');
/**
* DB connection identifier
*/
define('DATABASE', 'localhost:pooled');
/**
* DB character set for returned data
*/
define('CHARSET', 'UTF8');
/**
* Client Information text for DB tracing
*/
define('CLIENT_INFO', 'AnyCo Corp.');
?>
Oracle DBに接続するには、ユーザー名、パスワードおよび接続先DBを識別するための文字列が必要になります。これらは、PHPのdefine()コマンドを使用し、定数SCHEMA、PASSWORDおよびDATABASEとして設定します。キャラクタ・セットはオプションですが、推奨されている接続パラメータです。ここでは、CHARSET定数にUTF8が選択されています。
ほとんどのPHPアプリケーションは、常時1つのデータベース・アカウントを使用してDBに接続します。この例では、データベース・ユーザーはHRです。このことは、軽視できないセキュリティ上の意味合いを含んでいます。ファイルの拡張子が.phpだとしても、実際には、資格証明やその他のセキュリティ情報を記述したファイルはApacheがアクセスするディレクトリとは別の場所に格納することが推奨されており、これらのファイルはPHPのrequire()コマンドを使用してロードします。ファイルに資格証明をハードコーディングするのを避けるため、サイトによってはアプリケーションは、Apacheの起動前に設定される環境変数から、値を読み取る必要があります。
DATABASEで使用されるデータベース接続構文は簡易接続構文です。これはデータベースが実行されているホスト名を指定し、データベースのサービス名を特定します。ここで指定されているコンピュータはlocalhostで、これはPHPとデータベースを同じコンピュータ上に置く必要があることを意味しています。接尾辞:pooledは、接続でDRCPプールを使用する必要があることを示しています。「WindowsおよびLinuxでのPHPインストール後のタスク」の項でDRCPプールを起動していない場合は、この接尾辞を省略して、DATABASEをlocalhostに変更します。DRCPを使用するかどうかを決定するのに必要なアプリケーション変更は、この箇所のみです。
サイトの規準に応じて、接続識別子をOracle Netのtnsnames.oraの別名にすることもできます。
CLIENT_INFO定数は、データベース内でアプリケーションのエンドツーエンドの追跡を行うために使用されます。これについては、第13章「アプリケーションのデータベース使用状況の監視」で説明します。
新規のPHPファイルac_db.inc.phpを作成し、データベース・アクセス・クラスを含めます。初期のファイルには次が含まれています。
<?php
/**
* ac_db.inc.php: Database class using the PHP OCI8 extension
* @package Oracle
*/
namespace Oracle;
require('ac_cred.inc.php');
/**
* Oracle Database access methods
* @package Oracle
* @subpackage Db
*/
class Db {
/**
* @var resource The connection resource
* @access protected
*/
protected $conn = null;
/**
* @var resource The statement resource identifier
* @access protected
*/
protected $stid = null;
/**
* @var integer The number of rows to prefetch with queries
* @access protected
*/
protected $prefetch = 100;
}
?>
ac_db.inc.phpファイルではネームスペースをOracleに設定して、ファイルで宣言または使用されるクラスのネームスペースを定義しています。これにより、アプリケーション内で偶然同じ名前のクラスが複数実装された場合の衝突を回避します。
データベース資格証明は、require()を使用してインクルードされます。要求されたファイルが存在しなかった場合、コンパイル・エラーが発生します。PHPには、ファイルが存在しない場合のエラーを表示しないinclude()関数もあります。require_once()およびinclude_once()といった異なる形を使用して、サブ・ファイルが複数回インクルードされるのを防ぐこともできます。
Dbクラスの属性については後述します。
コメントはオープン・ソース・ツールPHPDocumentorが解析できる形式です。たとえば、@packageは、このファイルが所属している全体のパッケージを定義しています。NetBeans 7.0では、これらのタグを使用してアプリケーション・ドキュメントを自動生成できます。
Dbクラスの$prefetch属性と閉じ括弧の間に、次の2つのメソッドを追加します。
/**
* Constructor opens a connection to the database
* @param string $module Module text for End-to-End Application Tracing
* @param string $cid Client Identifier for End-to-End Application Tracing
*/
function __construct($module, $cid) {
$this->conn = @oci_pconnect(SCHEMA, PASSWORD, DATABASE, CHARSET);
if (!$this->conn) {
$m = oci_error();
throw new \Exception('Cannot connect to database: ' . $m['message']);
}
// Record the "name" of the web user, the client info and the module.
// These are used for end-to-end tracing in the DB.
oci_set_client_info($this->conn, CLIENT_INFO);
oci_set_module_name($this->conn, $module);
oci_set_client_identifier($this->conn, $cid);
}
/**
* Destructor closes the statement and connection
*/
function __destruct() {
if ($this->stid)
oci_free_statement($this->stid);
if ($this->conn)
oci_close($this->conn);
}
PHPオブジェクトのインスタンスを作成するときには、__construct()メソッドがコールされます。DbクラスのコンストラクタがOracle Databaseへの接続をオープンし、文の実行時に使用できるように、その接続リソースを$conn属性で保持します。接続に失敗した場合、エラーが生成されます。PHPのphp.iniパラメータdisplay_errorsがOnの場合は、このエラーはユーザーに表示され、log_errorsがOnの場合は、Apacheのログ・ファイルに送信されます。「WindowsおよびLinuxでのPHPインストール後のタスク」の項では、開発の助けとなるようにdisplay_errorsがOnに設定されています。情報漏えいにつながるため、本番アプリケーションではユーザーにエラーが表示されないようにする必要があります。
コンストラクタによって、接続資格証明がoci_pconnect()関数に渡されます。AnyCoアプリケーションでは、「データベース常駐の接続プール」の説明のとおり、oci_pconnect()を使用して永続DRCP接続を作成します。
キャラクタ・セットもoci_pconnect()に渡されます。これはデータがOracleからPHPに返されるときのキャラクタ・セットを指定します。設定はオプションですが、設定することをお薦めします。キャラクタ・セットがoci_pconnect()に渡されなかった場合、PHPは環境設定からキャラクタ・セットを判断します。この場合処理が遅くなり、予期しない値が使用される可能性もあります。
1つのデータベース・ユーザー名を使用するということは、アプリケーション内のすべての文がHRによって実行されたものとしてデータベースに記録されるということです。そのため、分析や追跡が困難になったり、まったくできなくなることもあります。oci_set_client_identifier()関数を使用すると、接続とデータベースで処理された文の詳細とともに、任意の文字列を記録できます。識別子をWebユーザーの名前に設定することで、データベース管理者はエンド・ユーザーとデータベースの使用状況を明示的に関連付けることができます。次のドキュメントは、Oracle Databaseでクライアント識別子を使用できる箇所について説明しています。
http://www.oracle.com/technetwork/articles/dsl/php-web-auditing-171451.html
また、データベース追跡を支援するため各接続には、クライアント情報とモジュール名という2つのメタデータが設定されます。第13章「アプリケーションのデータベース使用状況の監視」では、これらの便利な使用方法について説明します。
接続エラーが発生した場合、例外がスローされます。例外クラスの名前は完全修飾の名前です。先頭の'\'を削除すると、\Oracle\Exceptionのコールが試行されるため、ランタイム・エラーが発生します。これは、ExceptionというクラスがOracleネームスペースで定義されていないためです。PHPのネームスペース・セパレータはバックスラッシュ(\)です。PHP 5.3にネームスペースが導入された際、使用可能であった唯一の文字がバックスラッシュでした。
Dbインスタンスのデストラクタは、すべてのオープン接続を明示的にクローズします。説明したようなDRCPプール永続接続の場合、再利用に備えデータベース・サーバー・プロセスをDRCPプールに戻します。PHP変数は内部で参照カウント・メカニズムを使用しており、接続リソースの参照カウントを増加させる変数はすべて、その背後にあるデータベース接続が物理的にクローズされる前に解放される必要があります。ここでは、これは文リソースをクローズするという意味であり、このマニュアルでは文を実行するためのクラス拡張の箇所でこれを使用します。
PHPの参照カウント・メカニズムのため、説明したデストラクタはオブジェクトのインスタンスが破棄されるときに、単にデフォルトの動作をエミュレートします。文と接続のリソースは、それらを参照している変数が破棄されたときに終了します。そのため、この特殊なデストラクタ実装は省略できます。
PHP OCI8での文の実行には、文のテキストの解析と実行が含まれます。手続き型の手法では、INSERTは次のようになります。
$c = oci_pconnect($un, $pw, $db, $cs);
$sql = "INSERT INTO mytable (c1, c2) VALUES (1, 'abc')";
$s = oci_parse($c, $sql);
oci_execute($s);
データベース・システム内で、ある文が別のデータ値で再実行される場合、バインド変数を使用します。
$c = oci_pconnect($un, $pw, $db, $cs);
$sql = "INSERT INTO mytable (c1, c2) VALUES (:c1_bv, :c2_bv)";
$s = oci_parse($c, $sql);
$c1 = 1;
$c2 = 'abc';
oci_bind_by_name($s, ":c1_bv", $c1, -1);
oci_bind_by_name($s, ":c2_bv", $c2, -1);
oci_execute($s);
バインディングはPHP変数を、SQL文内のバインド識別子プレースホルダに関連付けます。バインド長は-1に設定されます。これは、内部バッファ・サイズをPHP値の長さから推測するようPHPに指示します。oci_bind_by_name()を使用してデータベースからデータを取得する場合は(PL/SQL関数の戻り値をバインド変数に代入するような場合)、実際に予測されるデータ長を指定して、PHP変数に十分な内部スペースが割り当てられるようにする必要があります。
バインド変数はパフォーマンスとセキュリティの面で重要です。これによりデータベースは、同じ文を異なる変数値を使用して繰り返し実行するときに、文メタデータを再利用できます。このかわりに使用できるPHPのコーディング形式では、PHP変数値を連結してSQL文のテキストを生成します。DBはこれらの文をそれぞれ一意の文として認識するため、キャッシュの利用が少なくなります。これはDBのパフォーマンスに大きな影響を与えます。連結には、悪質なユーザー入力による連結によってSQL文のセマンティクスが変更されるといった、SQLインジェクションのセキュリティ・リスクもあります。
PHP内でSQL問合せは実行とほぼ同じですが、その後にフェッチ・コール(このPHPにはいくつかの異形があります)が続きます。たとえば、すべての行を一度にフェッチするには次のようにします。
$c = oci_pconnect($un, $pw, $db, $cs);
$sql = "SELECT * FROM mytable WHERE c1 = :c1_bv AND c2 = :c2_bv";
$s = oci_parse($c, $sql);
$c1 = 1;
$c2 = 'abc';
oci_bind_by_name($s, ":c1_bv", $c1, -1);
oci_bind_by_name($s, ":c2_bv", $c2, -1);
oci_execute($s);
oci_fetch_all($s, $res, 0, -1, OCI_FETCHSTATEMENT_BY_ROW);
問合せ結果は$resに格納されます。OCI_FETCHSTATEMENT_BY_ROW定数は、結果が各行のエントリを含む配列になることを示しています。行自体はサブ配列で表現されます。
問合せから大量の行が返される場合、メモリーが大量に使用される恐れがあります。oci_fetch_array()などのPHP OCI8関数をかわりにコールできます。この関数は結果セットの1行のみを返します。スクリプトが1行の処理を終えた後、oci_fetch_array()を再びコールして、次の行をフェッチできます。
ac_db.inc.phpのDbクラスの使い勝手をよくするため、次の2つのメソッドをクラスに追加します。
/**
* Run a SQL or PL/SQL statement
*
* Call like:
* Db::execute("insert into mytab values (:c1, :c2)",
* "Insert data", array(array(":c1", $c1, -1),
* array(":c2", $c2, -1)))
*
* For returned bind values:
* Db::execute("begin :r := myfunc(:p); end",
* "Call func", array(array(":r", &$r, 20),
* array(":p", $p, -1)))
*
* Note: this performs a commit.
*
* @param string $sql The statement to run
* @param string $action Action text for End-to-End Application Tracing
* @param array $bindvars Binds. An array of (bv_name, php_variable, length)
*/
public function execute($sql, $action, $bindvars = array()) {
$this->stid = oci_parse($this->conn, $sql);
if ($this->prefetch >= 0) {
oci_set_prefetch($this->stid, $this->prefetch);
}
foreach ($bindvars as $bv) {
// oci_bind_by_name(resource, bv_name, php_variable, length)
oci_bind_by_name($this->stid, $bv[0], $bv[1], $bv[2]);
}
oci_set_action($this->conn, $action);
oci_execute($this->stid); // will auto commit
}
/**
* Run a query and return all rows.
*
* @param string $sql A query to run and return all rows
* @param string $action Action text for End-to-End Application Tracing
* @param array $bindvars Binds. An array of (bv_name, php_variable, length)
* @return array An array of rows
*/
public function execFetchAll($sql, $action, $bindvars = array()) {
$this->execute($sql, $action, $bindvars);
oci_fetch_all($this->stid, $res, 0, -1, OCI_FETCHSTATEMENT_BY_ROW);
$this->stid = null; // free the statement resource
return($res);
}
これらのメソッドの機能は前述の手続き型のサンプルと同じですが、Actionというデータベース追跡メタデータと、問合せのパフォーマンスを調整するプリフェッチと呼ばれる方法がさらに追加されています。プリフェッチについては、第8章「問合せのパフォーマンスとプリフェッチ」で後述します。
Dbクラスに設定する追跡メタデータはすべてオプションですが、後付けするよりも設計に含めておくほうが簡単です。これがないと、本番アプリケーションのパフォーマンス問題やアクセス問題のトラブルシューティングが非常に困難になります。
文識別子リソース$this->stidをnullに設定すると、oci_free_statement() (デストラクタで使用)と同様の内部クリーン・アップが開始され、その後のメソッドの正当性テストのため、属性がnullに設定されます。
このDb::execute()メソッドでは、次のようにINSERT文を記述できます。
$db = new \Oracle\Db("Test Example", "Chris");
$sql = "INSERT INTO mytable (c1, c2) VALUES (:c1_bv, :c2_bv)";
$c1 = 1;
$c2 = 'abc';
$db->execute($sql, "Insert Example", array(array(":c1_bv", $c1, -1),
array(":c2_bv", $c2, -1)));
次に問合せの例を示します。
$db = new \Oracle\Db("Test Example", "Chris");
$sql = "SELECT * FROM mytable WHERE c1 = :c1_bv AND c2 = :c2_bv";
$c1 = 1;
$c2 = 'abc';
$res = $db->execFetchAll($sql, "Query Example",
array(array(":c1_bv", $c1, -1),
array(":c2_bv", $c2, -1)));
Dbインスタンスの作成では、完全修飾ネームスペース記述を使用します。
バインド変数は配列の配列内にカプセル化されます。各サブ配列が1つのバインド変数を表します。
コードのとおり、Dbクラスは、oci_execute()がコールされるたび自動的にコミットされます。これは、このクラスを将来のアプリケーションで再利用する場合に、パフォーマンスおよびトランザクションの整合性の面で意味を持ちます。より汎用的なDbにするには、次を実行するようにDb::execute()を変更することを検討します。
...
oci_execute($this->stid, OCI_NO_AUTO_COMMIT);
...
この場合、oci_commit()をコールするcommitメソッドとoci_rollback()をコールするrollbackメソッドを、Dbクラスに追加する必要があります。このマニュアルの例では、これらの変更は必要ありません。PHPにおいて、同じ接続資格証明が使用されるoci_connect()またはoci_pconnect()のコールでは、その背後で同一のデータベース接続が再利用されます。そのため、アプリケーションがDbインスタンスを2つ作成した場合、それらのトランザクション状態は同じになります。インスタンスをロールバックまたはコミットすると、もう一方のトランザクションにも影響があります。oci_new_connect()関数はこれと異なり、コールされるたびに専用の新規接続を作成します。
test_db.phpという新規のPHPファイルを作成して、Dbクラスをテストします。
<?php
// test_db.php
require('ac_db.inc.php');
$db = new \Oracle\Db("test_db", "Chris");
$sql = "SELECT first_name, phone_number FROM employees ORDER BY employee_id";
$res = $db->execFetchAll($sql, "Query Example");
// echo "<pre>"; var_dump($res); echo "</pre>\n";
echo "<table border='1'>\n";
echo "<tr><th>Name</th><th>Phone Number</th></tr>\n";
foreach ($res as $row) {
$name = htmlspecialchars($row['FIRST_NAME'], ENT_NOQUOTES, 'UTF-8');
$pn = htmlspecialchars($row['PHONE_NUMBER'], ENT_NOQUOTES, 'UTF-8');
echo "<tr><td>$name</td><td>$pn</td></tr>\n";
}
echo "</table>";
?>
require()コマンドでac_db.inc.phpのコンテンツを含め、Dbクラスへのスクリプト・アクセスを付与しています。
Dbインスタンス作成のモジュール名パラメータは、ファイル名ベースtest_dbに設定します。これにより、データベース追跡で接続の開始元を特定できるようになります。接続識別子は架空のユーザー名に自由に設定します。$db->execFetchAll()へのActionパラメータは、ファイル内の操作に設定します。
この例ではバインド変数は渡さないため、$db->execFetchAll()メソッド・コールでオプションのバインド変数は指定しません。Db::execFetchAll()の定義で、最後の引数がないときにバインド変数リストを空の配列に設定しているため、データのバインドは行われません。
問合せ結果は行データの配列として$resに代入されます。var_dump()関数を非コメント化すると配列構造を確認できるので、単純なPHPのデバッグに有用です。$res配列がforeach()ループで反復処理され、各行が順番に処理されます。各行のサブ配列に入っている2つの列には、$row['FIRST_NAME']と$row['PHONE_NUMBER']でアクセスできます。Oracle Databaseの表の列は、デフォルトで大文字と小文字が区別されません。これらは大文字の配列索引としてPHPに返されます。次のように大文字と小文字を区別する列名を使用して、Oracleで表が作成されているとします。
CREATE TABLE mytab ("MyCol" NUMBER);
この場合、PHPで大文字と小文字を区別する配列索引$row['MyCol']を使用する必要があります。
test_db.phpでは、戻りデータをhtmlspecialchars()で処理し、HTMLに解釈されてしまう可能性を含むテキストを、HTMLマークアップではなく表示可能なテキストとして処理されるようにしています。このような出力のエスケープ処理は、Webアプリケーションからクロスサイト・スクリプティング(XSS)のセキュリティ問題を排除するためのセキュリティ対策として、非常に重要です。
使用するhtmlspecialchars()オプションはコンテキストによって異なります。PHPのhtmlentities()関数も便利な関数です。キャラクタ・セットはHTMLページのキャラクタ・セットと一致している必要があります。AnyCoアプリケーションではこれを行います。
test_db.phpをブラウザにロードします(http://localhost/test_db.php)。または、NetBeansで「Projects」ナビゲータ内のファイルを右クリックし、「Run」を選択します。
次のように表示されます。

接続で問題が発生した場合は、PHPインタプリタ・エラーをすべて解決します。すべてのメソッドがクラス定義の括弧内に配置されていることを確認します。その他の一般的な問題については、項「OracleへのPHP接続のテスト」を参照してください。