この章では、前の章で作成した「AnyCo Corp. Employees List」ページで従業員名の横にある「Show」リンクをクリックしたときに実行されるレポートを作成します。
前の章ではSQL問合せからデータをフェッチする方法を説明しました。この章では、PHPでREF CURSOR
を使用する方法について説明します。従業員に支給された備品の名前をREF CURSOR
でフェッチします。
この章のトピックは、次のとおりです。
PL/SQLパッケージには、関連する項目が単一の論理エンティティとして格納されます。パッケージは、次の2つの部分で構成されます。
パッケージ仕様部は、パッケージの内容物を定義します。C++言語などのヘッダー・ファイルに似ています。仕様部はすべてのパブリック項目を定義します。仕様部はパッケージへの公開インタフェースです。
パッケージ本体には、仕様部で定義したプロシージャおよびファンクションのコードと、仕様部で宣言していないプライベート・プロシージャおよびファンクションのコードが含まれます。プライベート・コードはパッケージ本体内でのみ確認できます。
パッケージ仕様部とパッケージ本体は、データ・ディクショナリに別々のオブジェクトとして格納され、user_source
ビューで確認できます。仕様部はPACKAGE
型として格納され、本体はPACKAGE BODY
型として格納されます。
一連のパブリック定数を宣言する場合と同様、本体のない仕様部を保持することはできますが、仕様部のない本体を保持することはできません。
ストアド・プロシージャとは、ある操作を実行するために設計された一連のPL/SQL文を1つにまとめて名前を付けたものです。ストアド・プロシージャはデータベース内に格納されます。これは、クライアント・アプリケーションがデータベース・オブジェクトと直接対話できるようにするものではなく、データベースのプログラミング・インタフェースを定義するものです。一般的にストアド・プロシージャを使用するのは、データを検証する場合や、複数のSQL問合せを組み合せた大規模で複雑な処理の指示をカプセル化する場合です。
ストアド・ファンクションは、戻り値パラメータを1つとります。ファンクションとは異なり、プロシージャは値を戻す場合と戻さない場合があります。
REF
CURSOR
を使用することは、Oracle Databaseからの問合せ結果をクライアント・アプリケーションに戻す最も強力かつ柔軟で、拡張性のある方法の1つです。
REF
CURSOR
はPL/SQLデータ型であり、この値はデータベース上の問合せ作業領域のメモリー・アドレスです。つまり、REF
CURSOR
は、データベース上にある結果セットへのポインタまたはハンドルとなります。
REF
CURSOR
には次の特性があります。
REF
CURSOR
は、データベース上のメモリー・アドレスを参照します。したがって、クライアントがデータベースにアクセスするには、REF
CURSOR
の存続中はデータベースに接続している必要があります。
REF
CURSOR
により、追加のデータベース・ラウンドトリップが発生します。REF
CURSOR
がクライアントに戻されても、クライアントがREF
CURSOR
をオープンしてデータをリクエストするまで、実際のデータは戻されません。ユーザーがその読取りを試行するまで、データは取得されません。
REF
CURSOR
は更新できません。REF
CURSOR
で表される結果セットは読取り専用です。REF
CURSOR
を使用してデータベースを更新することはできません。
REF
CURSOR
は後方にスクロールできません。REF
CURSOR
で表される結果セットは、前進専用で順次アクセスされます。結果セット内のレコードをランダムにポイントするためにREF
CURSOR
内にレコード・ポインタを配置することはできません。
REF
CURSOR
はPL/SQLデータ型です。PL/SQLコード・ブロック内でREF
CURSOR
を作成して戻すことができます。
このマニュアルの例では、AnyCo Corpが各従業員に職務に必要な備品を支給するというシナリオを使用します。EQUIPMENT
表には、備品名と支給先の従業員が格納されます。
SQL*Plusで、HR
ユーザーとして接続し、次のスクリプトを実行します。
sqlplus hr/welcome@localhost
CREATE TABLE equipment( id NUMBER PRIMARY KEY, employee_id REFERENCES employees(employee_id) ON DELETE CASCADE, equip_name VARCHAR2(20) NOT NULL); CREATE SEQUENCE equipment_seq; CREATE TRIGGER equipment_trig BEFORE INSERT ON equipment FOR EACH ROW BEGIN :NEW.id := equipment_seq.NEXTVAL; END; /
PL/SQLシーケンスおよびトリガーによって、各新規従業員レコードが挿入される際、レコードに一意のキーが割り当てられます。
これらの文をNetBeansなどのSQLエディタで実行する場合は、CREATE TRIGGER
文の末尾のスラッシュ('/
')を省略します。SQL*Plus内でスラッシュは文の終わりを示すインジケータであり、データベースで実行する文の一部ではありません。
サンプル・データを作成します。
-- Sample Data INSERT INTO equipment (employee_id, equip_name) VALUES (100, 'pen'); INSERT INTO equipment (employee_id, equip_name) VALUES (100, 'telephone'); INSERT INTO equipment (employee_id, equip_name) VALUES (101, 'pen'); INSERT INTO equipment (employee_id, equip_name) VALUES (101, 'paper'); INSERT INTO equipment (employee_id, equip_name) VALUES (101, 'car'); INSERT INTO equipment (employee_id, equip_name) VALUES (102, 'pen'); INSERT INTO equipment (employee_id, equip_name) VALUES (102, 'paper'); INSERT INTO equipment (employee_id, equip_name) VALUES (102, 'telephone'); INSERT INTO equipment (employee_id, equip_name) VALUES (103, 'telephone'); INSERT INTO equipment (employee_id, equip_name) VALUES (103, 'computer'); INSERT INTO equipment (employee_id, equip_name) VALUES (121, 'computer'); INSERT INTO equipment (employee_id, equip_name) VALUES (180, 'pen'); INSERT INTO equipment (employee_id, equip_name) VALUES (180, 'paper'); INSERT INTO equipment (employee_id, equip_name) VALUES (180, 'cardboard box'); COMMIT;
SQL*Plusで、HR
としてプロシージャを作成します。
CREATE OR REPLACE PROCEDURE get_equip(eid_p IN NUMBER, RC OUT SYS_REFCURSOR) AS BEGIN OPEN rc FOR SELECT equip_name FROM equipment WHERE employee_id = eid_p ORDER BY equip_name; END; /
PHPでは、匿名PL/SQLブロックを実行することで、このプロシージャをコールできます。
BEGIN get_equip(:id, :rc); END;
:id
バインド変数は前述のバインドと同じように使用されます。get_equip()
のWHERE
句に使用するため、PHP変数からの値をデータベースに渡します。バインド変数:rc
はこれと異なり、後述するとおりequip_name()
から返された問合せ結果を格納します。
従業員の備品リストを表示するため、新規のPHPファイルac_show_equip.php
を作成します。
<?php /** * ac_show_equip.php: Show an employee's equipment * @package ShowEquipment */ session_start(); require('ac_db.inc.php'); require('ac_equip.inc.php'); $sess = new \Equipment\Session; $sess->getSession(); if (!isset($sess->username) || empty($sess->username) || !isset($_GET['empid'])) { header('Location: index.php'); exit; } $empid = (int) $_GET['empid']; $page = new \Equipment\Page; $page->printHeader("AnyCo Corp. Show Equipment"); $page->printMenu($sess->username, $sess->isPrivilegedUser()); printcontent($sess, $empid); $page->printFooter(); // Functions ?>
これはac_emp_list.php
とほぼ同じ構造です。今回は、$sess->getSession()
の後の検証テストでも従業員識別子をチェックします。この値は、ac_emp_list.php
のprintrecords()
関数からURLパラメータとして渡されます。
... <a href='ac_show_equip.php?empid=$eid'>Show</a> ...
識別子の値はac_show_equip.php
で、PHPの$_GET
スーパーグローバル配列をとおしてアクセスされます。配列エントリが設定されていない場合、ac_show_equip.php
が不適切にコールされたものと見なされ、ユーザーはログイン・ページindex.php
にリダイレクトされます。
$_GET['empid']
値を整数にキャストし、SQLインジェクションの危険性を極力少なくしています。この値をバインドしますが、すべてのユーザー入力を一貫してフィルタすることをお薦めします。$_GET['empid']
になんらかの理由でアルファベットのテキストが格納された場合、PHPのキャスト・ルールの結果、$empid
に数字0
が格納されます。テキストの先頭が数字であった場合、$empid
はその数字になりますが、少なくともそれに続くテキストは破棄されます。
ファイルのメイン・コンテンツに入る前に、ac_show_equip.php
のFunctionsセクションに、小さいヘルパー関数getempname()
を追加します。
/** * Get an Employee Name * * @param Db $db * @param integer $empid * @return string An employee name */ function getempname($db, $empid) { $sql = "SELECT first_name || ' ' || last_name AS emp_name FROM employees WHERE employee_id = :id"; $res = $db->execFetchAll($sql, "Get EName", array(array(":id", $empid, -1))); $empname = $res[0]['EMP_NAME']; return($empname); }
これはスクリプトを実行する従業員識別子を受け取り、一致する従業員名をルック・アップします。演習として、問合せで行が返されなかった場合の処置を加えてください。
メインのprintcontent()
関数をac_show_equip.php
に追加します。
/** * Print the main body of the page * * @param Session $sess * @param integer $empid Employee identifier */ function printcontent($sess, $empid) { echo "<div id='content'>\n"; $db = new \Oracle\Db("Equipment", $sess->username); $empname = htmlspecialchars(getempname($db, $empid), ENT_NOQUOTES, 'UTF-8'); echo "$empname has: "; $sql = "BEGIN get_equip(:id, :rc); END;"; $res = $db->refcurExecFetchAll($sql, "Get Equipment List", "rc", array(array(":id", $empid, -1))); if (empty($res['EQUIP_NAME'])) { echo "no equipment"; } else { echo "<table border='1'>\n"; foreach ($res['EQUIP_NAME'] as $item) { $item = htmlspecialchars($item, ENT_NOQUOTES, 'UTF-8'); echo "<tr><td>$item</td></tr>\n"; } echo "</table>\n"; } echo "</div>"; // content }
これは、新規のメソッドDb::refcurExecFetchAll()
をコールします。レコードの配列が返され、慣習的なループの方法で出力されます。
REF CURSOR
バインド・パラメータ:rc
は、特別にバインドする必要があります。バインド変数名は文テキストの任意の場所で任意に選択または配置できるため、その名前は単独でrefcurExecFetchAll()
に渡され、通常のバインド変数の配列には格納されません。
ac_db.inc.php
を編集しrefcurExecFetchAll()
メソッドを作成し、これをDb
クラスに追加します。
/** * Run a call to a stored procedure that returns a REF CURSOR data * set in a bind variable. The data set is fetched and returned. * * Call like Db::refcurexecfetchall("begin myproc(:rc, :p); end", * "Fetch data", ":rc", array(array(":p", $p, -1))) * The assumption that there is only one refcursor is an artificial * limitation of refcurexecfetchall() * * @param string $sql A SQL string calling a PL/SQL stored procedure * @param string $action Action text for End-to-End Application Tracing * @param string $rcname the name of the REF CURSOR bind variable * @param array $otherbindvars Binds. Array (bv_name, php_variable, length) * @return array Returns an array of tuples */ public function refcurExecFetchAll($sql, $action, $rcname, $otherbindvars = array()) { $this->stid = oci_parse($this->conn, $sql); $rc = oci_new_cursor($this->conn); oci_bind_by_name($this->stid, $rcname, $rc, -1, OCI_B_CURSOR); foreach ($otherbindvars 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); oci_execute($rc); // run the ref cursor as if it were a statement id oci_fetch_all($rc, $res); $this->stid = null; return($res); }
$rcname
内のREF CURSOR
バインド・パラメータは、通常のPHP変数ではなくoci_new_cursor
()で作成されたカーソルにバインドされます。型OCI_B_CURSOR
を指定する必要があります。
追跡するアクションのテキストを設定した後、PL/SQL文が実行されます。この例では、get_equip()
がコールされ、問合せのカーソルをオープンして返します。これで、$rc
内のREF CURSOR
をoci_parse()
コールから返された識別子のように、PHP文の識別子として扱うことが可能になりました。フェッチはここから行われます。問合せ結果は、$res
で関数のコール元に返されます。
すべてのファイルを保存し、ブラウザでアプリケーションを実行します。Simon
またはAdministrator
としてログインします。「Steven King」の横の「Show」リンクをクリックします。その個人が所有する備品が表示されます。