存儲過程是實際位於 Oracle 中的程序。大多數存儲過程都是用 PL/SQL 編寫的,在 Oracle 數據庫 10g 第 2 版和更高版本中,您能夠用 Java、.NET 或其餘語言將它們編寫爲外部過程。 存儲過程一般將一系列相關操做組成一個 API。存儲過程執行的操做包括由 SQL 語句以及 PL/SQL 語句執行的操做,SQL 語句用於獲取和修改數據,PL/SQL 語句將對這些數據進行相應操做,如執行某些數學運算、對值進行詳細驗證值以及處理錯誤條件。它們下降了調用程序與數據庫之間的「往返」次數並簡化了客戶端 中的數據管理邏輯,從而有利於提升性能。 若是考慮一下管理表之間的多對多關係一般須要的代碼,則會發現對現有數據執行更新一般涉及三個不一樣的查詢。經過將該進程封裝在單個存儲過程當中,將減小客戶 端與數據庫之間的通訊量,而一般須要在客戶端代碼分多個步驟執行的操做將減化爲一個數據庫調用。 PHP OCI8 擴展支持對存儲過程的調用,您能夠將參數綁定到過程語句(與將參數綁定到普通的 SQL 語句方法相同),並能夠訪問結果遊標和 Oracle 集合。本方法文檔中提供了存儲過程的常見操做示例。 存儲過程輸入和輸出 調用 Oracle 存儲過程時,全部輸入和輸出數據均以參數形式傳遞給過程。若是您習慣於使用某些參數調用 PHP 函數並讓它返回一個值的過程,那麼起初您可能對此感到有些疑惑不解,但經過示例卻能夠一目瞭然。 php
一,假設有如下存儲過程簽名: sql
sayHello (name IN VARCHAR2, greeting OUT VARCHAR2)
調用此過程時,第一個參數名將包含一個在調用時提供的輸入值,而 greeting 將由該過程填充,做爲一個「返回」值,在該過程完成後使用。 閱讀規範 PL/SQL 編程不是本方法文檔的範疇,但您須要對存儲過程有一個大體的瞭解並可以閱讀接口規範,但沒必要深究。 對於存儲過程的源代碼,開始都須要先定義接受的參數,例如: 數據庫
PROCEDURE edit_entry( status_out OUT NUMBER, status_msg_out OUT VARCHAR2, id_inout IN OUT INTEGER, title_in IN VARCHAR2, text_out OUT CLOB, categories_in IN list_of_numbers );
該過程名爲 edit_entry。圓括號中定義了能夠傳遞給該過程的各參數(由逗號分隔)。每一個參數中,你會看到用於在該過程內部引用其值的名稱(不須要在 PHP 腳本中使用同一名稱)、參數的模式(以下所示)以及該參數的類型。 對於該示例中的第一個參數: 編程
status_out OUT NUMBER,
內部名稱爲 status_out,模式爲 OUT,類型爲 NUMBER(它是一個原生的 Oracle 數據類型)。 後面有一個 id_inout 參數。 app
id_inout IN OUT INTEGER,
它的模式爲 IN OUT,類型爲 INTEGER。 最後是 categories_in 參數: 函數
categories_in IN list_of_numbers
此處的類型是由用戶定義的(稍後將對該類型進行詳細介紹)。 參數模式 參數模式描述了數據從調用方到過程的「流」向: oop
IN – 該模式的參數由調用方提供。 性能
OUT – 參數能夠由過程分配值並返回至調用方。 測試
IN OUT – 參數能夠在兩個「方向」使用;即,調用方能夠爲該參數提供值,而過程也能夠修改參數值。 fetch
參數項是必選項。從 PHP 調用過程時,必須將 PHP 變量綁定到它定義的全部參數。您沒必要向 PHP 變量分配值,即便它們是輸入參數 – 若是未向標量類型分配值,Oracle 將把它視爲 NULL 值。 值得注意的是,存儲過程能夠在 Oracle 中「重載」。換言之,能夠有兩個名稱相同但參數簽名不一樣的過程。將依據 PHP 變量綁定到的參數的數目和類型來決定要調用哪一個過程。 複雜類型 存儲過程使用的參數並不僅侷限於 VARCHAR2 和 INTEGER 等標量類型。也能夠傳遞並接收復雜的數據類型,如值列表或與從表中選擇的行集相對應的結果遊標。 通常說來,若是存在要迭代的數據行,則您將一般會收到從存儲過程返回的遊標,而若是您須要傳入值列表,則一般將使用集合。如下示例經過 PHP 演示了這些複雜類型。 調用方與定義方權限。Oracle 對「調用方」(執行存儲過程的用戶)和定義方(以其身份執行 CREATE PROCEDURE 語句的用戶)進行了區分。 默認狀況下,存儲過程是以定義方的權限執行的,即便調用方是不一樣的用戶。這意味着表的全部訪問權限(例如,在過程當中的訪問權限)將由定義方的權限控制,因 此調用方只須要執行過程的權限而非它使用的表的權限。 能夠在過程定義中用關鍵字 AUTHID CURRENT_USER 更改此模型。設置該指令後,執行存儲過程時所需的權限將在運行時依據執行該過程的當前用戶來決定。 該方法的一個用途是測試一個修改表數據但實際上不修改實時數據的過程。這種狀況下,調用方在他們本身的模式中定義一個表(該表與從他們須要執行的過程當中訪 問的表同名),而過程依據本地表而非提供給定義方的表執行。 從 PHP 中調用存儲過程 對於要從 PHP 中執行以調用過程的 SQL 語句而言,您將一般在 Oracle BEGIN ...END; 塊(稱做匿名塊)中嵌入調用。例如:
<?php // etc. //$sql = 'BEGIN sayHello(:name, :message); END;'; //而後,經過調用 oci_bind_by_name() 將參數綁定到 PHP 變量。 若是使用如下 DDL 語句定義了 sayHello //: //CREATE OR REPLACE PROCEDURE //sayHello (name IN VARCHAR2, greeting OUT VARCHAR2) //AS //BEGIN //greeting := 'Hello ' || name; //END; // //注意,您可使用 SQL*Plus 命令行運行上面的語句。將該語句保存到文件 (SAYHELLO.SQL)。接下來,使用 //SQL*Plus 登陸: // $ sqlplus username@SID // 而後,使用 START 命令建立該過程: // SQL> START /home/username/SAYHELLO.SQL // 如下 PHP 腳本調用該過程: $conn = oci_connect('SCOTT','TIGER') or die; $sql = 'BEGIN sayHello(:name, :message); END;'; $stmt = oci_parse($conn,$sql); // Bind the input parameter oci_bind_by_name($stmt,':name',$name,32); // Bind the output parameter oci_bind_by_name($stmt,':message',$message,32); // Assign a value to the input $name = 'Harry'; oci_execute($stmt); // $message is now populated with the output value print "$message\n"; ?>
Blog 示例程序包。爲演示調用存儲過程方面的某些技巧,您將在此處使用如下名爲 blog 的程序包,該程序包提供了一個 API,用於獲取和修改假設的網誌應用程序中的條目。程序包用於經過其自身的做用域將過程、函數和數據封裝在其自身的命名空間內部,並使它們獨立於全局數 據庫命名空間中的其餘過程。調用程序包中的過程時,將使用句號來分隔程序包名稱與過程名稱。 可使用如下語句指定 blog 程序包:
CREATE OR REPLACE PACKAGE blog AS TYPE cursorType IS REF CURSOR RETURN blogs%ROWTYPE; /* Fetch the latest num_entries_in from the blogs table, populating entries_cursor_out with the result */ PROCEDURE latest( num_entries_in IN NUMBER, entries_cursor_out OUT cursorType ); /* Edit a blog entry.If id_inout is NULL, results in an INSERT, otherwise attempts to UPDATE the existing blog entry. status_out will have the value 1 on success, otherwise a negative number on failure with status_msg_out containing a description categories_in is a collection where list_of_numbers is described by TYPE list_of_numbers AS VARRAY(50) OF NUMBER; */ PROCEDURE edit_entry( status_out OUT NUMBER, status_msg_out OUT VARCHAR2, id_inout IN OUT INTEGER, title_in IN VARCHAR2, text_out OUT CLOB, categories_in IN list_of_numbers ); END blog; /
該程序包提供了兩個過程:blog.latest(返回包含最新 num_entries 網誌條目的結果遊標)和 blog.edit_entry(容許插入新的網誌條目以及修改現有的網誌條目)。若是爲 id_inout 參數提供值,則該過程將嘗試更新具備該 id 的相應網誌條目。不然,它將插入一個新的網誌條目並使用新行的主鍵填充 id_inout。該過程還接受與網誌條目的主體相對應的 CLOB 對象以及與該條目歸檔到的類別列表相對應的集合對象。此處引用的集合類型 list_of_numbers
由如下語句定義:
CREATE OR REPLACE TYPE list_of_numbers AS VARRAY(50) OF NUMBER;
下面顯示了該程序包的主體。您能夠經過其中的註釋瞭解它的功能而沒必要深刻了解 PL/SQL:
CREATE OR REPLACE PACKAGE BODY blog AS /*------------------------------------------------*/ PROCEDURE latest( num_entries_in IN NUMBER, entries_cursor_out OUT cursorType ) AS BEGIN OPEN entries_cursor_out FOR SELECT * FROM blogs WHERE rownum < num_entries_in ORDER BY date_published DESC; END latest; /*------------------------------------------------*/ PROCEDURE edit_entry( status_out OUT NUMBER, status_msg_out OUT VARCHAR2, id_inout IN OUT INTEGER, title_in IN VARCHAR2, text_out OUT CLOB, categories_in IN list_of_numbers AS ENTRY_NOT_FOUND EXCEPTION; entry_found INTEGER := 0; BEGIN /* Default status to success */ status_out := 1; /* If id_inout has a value then attempt to UPDATE */ IF id_inout IS NOT NULL THEN /* Check the id exists - raise ENTRY_NOT_FOUND if not */ SELECT COUNT(*) INTO entry_found FROM blogs b WHERE b.id = id_inout; IF entry_found != 1 THEN RAISE ENTRY_NOT_FOUND; END IF; /* Update the blogs table returning the CLOB field */ UPDATE blogs b SET b.title = title_in, b.text = EMPTY_CLOB() WHERE b.id = id_inout RETURNING b.text INTO text_out; /* Remove any existing relationships to categories - new categories inserted below */ DELETE FROM blogs_to_categories WHERE blog_id = id_inout; status_msg_out := 'Blog entry ' || id_inout || ' updated'; /* id_inout was null so INSERT new record */ ELSE INSERT INTO blogs b ( b.id, b.title, b.date_published, b.text ) VALUES ( blog_id_seq.nextval, title_in, SYSDATE, EMPTY_CLOB() ) RETURNING b.id, b.text INTO id_inout, text_out; status_msg_out := 'Blog entry ' || id_inout || ' inserted'; END IF; /* Now handle assignment to categories. Loop over the categories_in collection, inserting the new category assignments */ FOR i IN 1 .. categories_in.count LOOP INSERT INTO blogs_to_categories (blog_id,category_id) VALUES (id_inout,categories_in(i)); END LOOP; status_msg_out := status_msg_out || ' - added to ' || categories_in.count || ' categories'; EXCEPTION /* Catch the exception when id_inout not found */ WHEN ENTRY_NOT_FOUND THEN status_out := -1001; status_msg_out := 'No entry found in table blogs with id = ' || id_inout; /* Catch any other exceptions raised by Oracle */ WHEN OTHERS THEN status_out := -1; status_msg_out := 'Error:' || TO_CHAR (SQLCODE) || SQLERRM; END edit_entry; END blog; / The underlying table structure the procedures are using is: CREATE SEQUENCE blog_id_seq INCREMENT BY 1; / CREATE TABLE blogs ( id NUMBER PRIMARY KEY, title VARCHAR2(200), date_published DATE, text CLOB ); / CREATE SEQUENCE category_id_seq INCREMENT BY 1; CREATE TABLE categories ( id NUMBER PRIMARY KEY, name VARCHAR2(30) UNIQUE ); / CREATE TABLE blogs_to_categories ( blog_id INTEGER NOT NULL REFERENCES blogs(id), category_id INTEGER NOT NULL REFERENCES categories(id), PRIMARY KEY (blog_id, category_id) ); /
存儲過程和引用遊標 看一下 blog.latest 過程,您將看到它返回一個用於迭代 blogs 錶行的引用遊標。 與直接從 SELECT 語句中訪問行相比,在 PHP 中使用遊標須要兩個額外的步驟。第一步是使用 oci_new_cursor() 函數(該函數隨後用於綁定到相應的參數)在 PHP 中準備一個遊標資源。執行 SQL 語句後,第二步是對遊標資源調用 oci_execute()。 如下 PHP 腳本演示了該過程:
<?php $conn = oci_connect('SCOTT','TIGER') or die; $sql = 'BEGIN blog.latest(:num_entries, :blog_entries); END;'; $stmt = oci_parse($conn, $sql); // Bind the input num_entries argument to the $max_entries PHP variable $max_entries = 5; oci_bind_by_name($stmt,":num_entries",$max_entries,32); // Create a new cursor resource $blog_entries = oci_new_cursor($conn); // Bind the cursor resource to the Oracle argument oci_bind_by_name($stmt,":blog_entries",$blog_entries,-1,OCI_B_CURSOR); // Execute the statement oci_execute($stmt); // Execute the cursor oci_execute($blog_entries); print "The $max_entries most recent blog entries\n"; // Use OCIFetchinto in the same way as you would with SELECT while ($entry = oci_fetch_assoc($blog_entries, OCI_RETURN_LOBS )) { print_r($entry); } ?>
存儲過程和 LOB Oracle Long 對象與存儲過程之間能夠進行相互傳遞,方法與內部的 SQL 之間進行的相互傳遞幾乎相同。 如下示例演示瞭如何使用 CLOB 調用 blog.edit_entry 過程。該示例未向 id 參數分配值,所以它至關於插入一個新的網誌條目:
<?php $conn = oci_connect('SCOTT','TIGER') or die; $sql = 'BEGIN blog.edit_entry(:status, :status_msg, :id, :title, :text, :categories); END;'; $stmt = oci_parse($conn,$sql); $title = 'This is a test entry'; oci_bind_by_name($stmt,":status",$status,32); oci_bind_by_name($stmt,":status_msg",$status_msg,500); oci_bind_by_name($stmt,":id",$id,32); oci_bind_by_name($stmt,":title",$title,200); // Explained in the next example...(use an empty value for now) $Categories = oci_new_collection($conn,'LIST_OF_NUMBERS'); oci_bind_by_name($stmt,':categories',$Categories,32,OCI_B_SQLT_NTY); // Create a new lob descriptor object $textLob = oci_new_descriptor($conn, OCI_D_LOB); // Bind it to the parameter oci_bind_by_name($stmt, ":text", $textLob, -1, OCI_B_CLOB); // Execute the statement but do not commit oci_execute($stmt, OCI_DEFAULT); // The status parameter will be negative if the procedure encountered a problem if ( !$status ) { // Rollback the procedure oci_rollback($conn); die ("$status_msg\n"); } // Save the body of the blog entry to the CLOB if ( !$textLob->save('This is the body of the test entry') ) { // Rollback the procedure oci_rollback($conn); die ("Error saving lob\n"); } // Everything OK so commit oci_commit($conn); print $status_msg."\n"; ?>
正如該腳本所演示的,關鍵問題是如何在使用 LOB 時處理事務。因爲更新 LOB 是一個分爲兩階段的過程,所以您在此處選擇將全部事務處理委託給 PHP 腳本。 注意,默認狀況下,Oracle 只容許在任何給定的會話中一次運行一個事務。這意味着從 PHP 調用的過程當中發出的 commit 或 rollback 語句將覆蓋對 oci_commit() 或 oci_rollback() 的調用。可使用匿名事務(使用位於過程定義內部的 pragma PRAGMA AUTONOMOUS_TRANSACTION 啓用)更改此行爲。例如,您能夠在從其餘過程當中調用的日誌記錄程序包中使用匿名事務;使用這一方法您能夠記錄有關存儲過程調用的信息,而不會干擾正在會話 中運行的事務。 存儲過程和集合 集合是一種用於將複雜數據類型傳遞到存儲過程當中的機制。在網誌應用程序中,能夠將網誌條目歸檔到多個分類中(與「blogs」表和 「categories」表之間的多對多關係相對應)。 必須在數據庫中全局定義 Oracle 中的集合類型,在本示例中,您將使用如下定義:
CREATE OR REPLACE TYPE list_of_numbers AS VARRAY(50) OF NUMBER;
該定義容許您一次最多向 50 個類別分配一個網誌條目,方法是將該類型的實例傳遞給 blog.edit_entry 過程。 在 PHP 中,集合由預約義的 PHP 類 OCI-Collection 表示。能夠經過調用 oci_new_collection() 函數建立此類實例。OCI-Collection 對象提供瞭如下方法:
append:將元素添加到集合末尾
assign:從現有集合中將元素添加到某個集合
assignElem:將值分配給集合,並標識應將該元素置於的集合中的索引位置
free:釋放與集合句柄關聯的資源
getElem:從集合中的特殊索引位置檢索元素
max:返回集合中的最大元素數
size:返回集合的當前大小
trim:從集合末尾刪除一些元素
此處,您只但願使用 append 方法,所以能夠將類別 ID 列表附加到過程調用。在如下示例中,您將更新在前一個示例中建立的現有網誌條目,方法是將它的 ID 傳遞給 blog.edit_entry 過程以及類別 id 列表:
<?php $conn = oci_connect('SCOTT','TIGER') or die; $sql = 'BEGIN blog.edit_entry(:status, :status_msg, :id, :title, :text, :categories); END;'; $stmt = oci_parse($conn, $sql); $id = 1; // ID of the new entry $title = 'This is a test entry (v2)'; oci_bind_by_name($stmt,":status",$status,32); oci_bind_by_name($stmt,":status_msg",$status_msg,500); oci_bind_by_name($stmt,":id",$id,32); oci_bind_by_name($stmt,":title",$title,200); $textLob = oci_new_descriptor($conn, OCI_D_LOB); oci_bind_by_name($stmt, ":text", $textLob, -1, OCI_B_CLOB); // Create an OCI-Collection object $Categories = oci_new_collection($conn,'LIST_OF_NUMBERS'); // Append some category IDs to the collection; $Categories->append(2); $Categories->append(4); $Categories->append(5); // Bind the collection to the parameter oci_bind_by_name($stmt,':categories',$Categories,-1,OCI_B_SQLT_NTY); oci_execute($stmt, OCI_DEFAULT); if ( !$status ) { oci_rollback($conn); die ("$status_msg\n"); } if ( !$textLob->save('This is the body of the test entry [v2]') ) { oci_rollback($conn); die ("Error saving lob\n"); } oci_commit($conn); print $status_msg."\n"; ?>
二,假設有如下存儲過程簽名:
check_login (name VARCHAR2,pass VARCHAR2)
其建表等步驟發如上,php腳本以下:
<?php function check_login($user, $pass) { $conn = ocilogon('user', 'pass', 'database'); $sql = 'begin :result := test.check_login(:user, :pass); end;'; $stmt = oci_parse($conn, $sql); $result = ''; oci_bind_by_name($stmt, ':user', $user, 32); oci_bind_by_name($stmt, ':pass', md5($pass), 32); oci_bind_by_name($stmt, ':result', $result, 10); oci_execute($stmt); ocilogoff($conn); return $result; } ?>
結論 您如今已經瞭解了有關如何從 PHP 中調用存儲過程(既包括只涉及標量數據類型的簡單過程,也包含更復雜的使用 LOB、遊標和集合的過程)的示例。還對存儲過程的定義進行了足夠的瞭解,能讀懂它們的 PL/SQL 規範,這樣您就能夠從 PHP 中正確地調用它們並綁定相應的類型。