callablestatement存储过程
概述?
CallableStatement 對象為所有的 DBMS 提供了一種以標(biāo)準(zhǔn)形式調(diào)用已儲存過程的方法。已儲存過程儲存在數(shù)據(jù)庫中。對已儲存過程的調(diào)用是 CallableStatement 對象所含的內(nèi)容。這種調(diào)用是用一種換碼語法來寫的,有兩種形式:一種形式帶結(jié)果參數(shù),另一種形式不帶結(jié)果參數(shù)(有關(guān)換碼語法的信息,參見第 4 節(jié)“語句”)。結(jié)果參數(shù)是一種輸出 (OUT) 參數(shù),是已儲存過程的返回值。兩種形式都可帶有數(shù)量可變的輸入(IN 參數(shù))、輸出(OUT 參數(shù))或輸入和輸出(INOUT 參數(shù))的參數(shù)。問號將用作參數(shù)的占位符。
在 JDBC 中調(diào)用已儲存過程的語法如下所示。注意,方括號表示其間的內(nèi)容是可選項(xiàng);方括號本身并不是語法的組成部份。
{call 過程名[(?, ?, ...)]}
返回結(jié)果參數(shù)的過程的語法為:
{? = call 過程名[(?, ?, ...)]}
不帶參數(shù)的已儲存過程的語法類似:
{call 過程名}
通常,創(chuàng)建 CallableStatement 對象的人應(yīng)當(dāng)知道所用的 DBMS 是支持已儲存過程的,并且知道這些過程都是些什么。然而,如果需要檢查,多種 DatabaseMetaData 方法都可以提供這樣的信息。例如,如果 DBMS 支持已儲存過程的調(diào)用,則 supportsStoredProcedures 方法將返回 true,而 getProcedures 方法將返回對已儲存過程的描述。
CallableStatement 繼承 Statement 的方法(它們用于處理一般的 SQL 語句),還繼承了 PreparedStatement 的方法(它們用于處理 IN 參數(shù))。CallableStatement 中定義的所有方法都用于處理 OUT 參數(shù)或 INOUT 參數(shù)的輸出部分:注冊 OUT 參數(shù)的 JDBC 類型(一般 SQL 類型)、從這些參數(shù)中檢索結(jié)果,或者檢查所返回的值是否為 JDBC NULL。
1 創(chuàng)建 CallableStatement 對象?
CallableStatement 對象是用 Connection 方法 prepareCall 創(chuàng)建的。下例創(chuàng)建 CallableStatement 的實(shí)例,其中含有對已儲存過程 getTestData 調(diào)用。該過程有兩個變量,但不含結(jié)果參數(shù):
CallableStatement cstmt = con.prepareCall(?
"{call getTestData(?, ?)}");
其中 ? 占位符為 IN、 OUT 還是 INOUT 參數(shù),取決于已儲存過程 getTestData。
2 IN 和 OUT 參數(shù)?
將 IN 參數(shù)傳給 CallableStatement 對象是通過 setXXX 方法完成的。該方法繼承自 PreparedStatement。所傳入?yún)?shù)的類型決定了所用的 setXXX 方法(例如,用 setFloat 來傳入 float 值等)。
如果已儲存過程返回 OUT 參數(shù),則在執(zhí)行 CallableStatement 對象以前必須先注冊每個 OUT 參數(shù)的 JDBC 類型(這是必需的,因?yàn)槟承?DBMS 要求 JDBC 類型)。注冊 JDBC 類型是用 registerOutParameter 方法來完成的。語句執(zhí)行完后,CallableStatement 的 getXXX 方法將取回參數(shù)值。正確的 getXXX 方法是為各參數(shù)所注冊的 JDBC 類型所對應(yīng)的 Java 類型(從 JDBC 類型到 Java 類型的標(biāo)準(zhǔn)映射見 8.6.1 節(jié)中的表)。換言之, registerOutParameter 使用的是 JDBC 類型(因此它與數(shù)據(jù)庫返回的 JDBC 類型匹配),而 getXXX 將之轉(zhuǎn)換為 Java 類型。
作為示例,下述代碼先注冊 OUT 參數(shù),執(zhí)行由 cstmt 所調(diào)用的已儲存過程,然后檢索在 OUT 參數(shù)中返回的值。方法 getByte 從第一個 OUT 參數(shù)中取出一個 Java 字節(jié),而 getBigDecimal 從第二個 OUT 參數(shù)中取出一個 BigDecimal 對象(小數(shù)點(diǎn)后面帶三位數(shù)):
CallableStatement cstmt = con.prepareCall(?
"{call getTestData(?, ?)}");?
cstmt.registerOutParameter(1, java.sql.Types.TINYINT);?
cstmt.registerOutParameter(2, java.sql.Types.DECIMAL, 3);?
cstmt.executeQuery();?
byte x = cstmt.getByte(1);?
java.math.BigDecimal n = cstmt.getBigDecimal(2, 3);
CallableStatement 與 ResultSet 不同,它不提供用增量方式檢索大 OUT 值的特殊機(jī)制。
3 INOUT 參數(shù)?
既支持輸入又接受輸出的參數(shù)(INOUT 參數(shù))除了調(diào)用 registerOutParameter 方法外,還要求調(diào)用適當(dāng)?shù)?setXXX 方法(該方法是從 PreparedStatement 繼承來的)。setXXX 方法將參數(shù)值設(shè)置為輸入?yún)?shù),而 registerOutParameter 方法將它的 JDBC 類型注冊為輸出參數(shù)。setXXX 方法提供一個 Java 值,而驅(qū)動程序先把這個值轉(zhuǎn)換為 JDBC 值,然后將它送到數(shù)據(jù)庫中。
這種 IN 值的 JDBC 類型和提供給 registerOutParameter 方法的 JDBC 類型應(yīng)該相同。然后,要檢索輸出值,就要用對應(yīng)的 getXXX 方法。例如,Java 類型為 byte 的參數(shù)應(yīng)該使用方法 setByte 來賦輸入值。應(yīng)該給 registerOutParameter 提供類型為 TINYINT 的 JDBC 類型,同時應(yīng)使用 getByte 來檢索輸出值 (第 8 節(jié)“JDBC 和 Java 類型之間的映射”將給出詳細(xì)信息和類型映射表)。
下例假設(shè)有一個已儲存過程 reviseTotal,其唯一參數(shù)是 INOUT 參數(shù)。方法 setByte 把此參數(shù)設(shè)為 25,驅(qū)動程序?qū)阉鳛?JDBC TINYINT 類型送到數(shù)據(jù)庫中。接著,registerOutParameter 將該參數(shù)注冊為 JDBC TINYINT。執(zhí)行完該已儲存過程后,將返回一個新的 JDBC TINYINT 值。方法 getByte 將把這個新值作為 Java byte 類型檢索。
CallableStatement cstmt = con.prepareCall(?
"{call reviseTotal(?)}");?
cstmt.setByte(1, 25);?
cstmt.registerOutParameter(1, java.sql.Types.TINYINT);?
cstmt.executeUpdate();?
byte x = cstmt.getByte(1);
4 先檢索結(jié)果,再檢索 OUT 參數(shù)?
由于某些 DBMS 的限制,為了實(shí)現(xiàn)最大的可移植性,建議先檢索由執(zhí)行 CallableStatement 對象所產(chǎn)生的結(jié)果,然后再用 CallableStatement.getXXX 方法來檢索 OUT 參數(shù)。
如果 CallableStatement 對象返回多個 ResultSet 對象(通過調(diào)用 execute 方法),在檢索 OUT 參數(shù)前應(yīng)先檢索所有的結(jié)果。這種情況下,為確保對所有的結(jié)果都進(jìn)行了訪問,必須對 Statement 方法 getResultSet、getUpdateCount 和 getMoreResults 進(jìn)行調(diào)用,直到不再有結(jié)果為止。
檢索完所有的結(jié)果后,就可用 CallableStatement.getXXX 方法來檢索 OUT 參數(shù)中的值。
5 檢索作為 OUT 參數(shù)的 NULL 值?
返回到 OUT 參數(shù)中的值可能會是 JDBC NULL。當(dāng)出現(xiàn)這種情形時,將對 JDBC NULL 值進(jìn)行轉(zhuǎn)換以使 getXXX 方法所返回的值為 null、0 或 false,這取決于 getXXX 方法類型。對于 ResultSet 對象,要知道 0 或 false 是否源于 JDBC NULL 的唯一方法,是用方法 wasNull 進(jìn)行檢測。如果 getXXX 方法讀取的最后一個值是 JDBC NULL,則該方法返回 true,否則返回 flase。第 5 節(jié)“ResultSet”將給出詳細(xì)信息。??
?
本文闡述了怎么使用DBMS存儲過程。我闡述了使用存儲過程的基本的和高級特性,比如返回ResultSet。本文假設(shè)你對DBMS和JDBC已經(jīng)非常熟 悉,也假設(shè)你能夠毫無障礙地閱讀其它語言寫成的代碼(即不是Java的語言),但是,并不要求你有任何存儲過程的編程經(jīng)歷。?
存儲過程是指保存在數(shù)據(jù)庫并在數(shù)據(jù)庫端執(zhí)行的程序。你可以使用特殊的語法在Java類中調(diào)用存儲過程。在調(diào)用時,存儲過程的名稱及指定的參數(shù)通過JDBC連接發(fā)送給DBMS,執(zhí)行存儲過程并通過連接(如果有)返回結(jié)果。?
使用存儲過程擁有和使用基于EJB或CORBA這樣的應(yīng)用服務(wù)器一樣的好處。區(qū)別是存儲過程可以從很多流行的DBMS中免費(fèi)使用,而應(yīng)用服務(wù)器大都非常昂 貴。這并不只是許可證費(fèi)用的問題。使用應(yīng)用服務(wù)器所需要花費(fèi)的管理、編寫代碼的費(fèi)用,以及客戶程序所增加的復(fù)雜性,都可以通過DBMS中的存儲過程所整個 地替代。?
你可以使用Java,Python,Perl或C編寫存儲過程,但是通常使用你的DBMS所指定的特定語言。Oracle使用 PL/SQL,PostgreSQL使用pl/pgsql,DB2使用Procedural SQL。這些語言都非常相似。在它們之間移植存儲過程并不比在Sun的EJB規(guī)范不同實(shí)現(xiàn)版本之間移植Session Bean困難。并且,存儲過程是為嵌入SQL所設(shè)計(jì),這使得它們比Java或C等語言更加友好地方式表達(dá)數(shù)據(jù)庫的機(jī)制。?
因?yàn)榇鎯^程運(yùn)行在DBMS自身,這可以幫助減少應(yīng)用程序中的等待時間。不是在Java代碼中執(zhí)行4個或5個SQL語句,而只需要在服務(wù)器端執(zhí)行1個存儲過程。網(wǎng)絡(luò)上的數(shù)據(jù)往返次數(shù)的減少可以戲劇性地優(yōu)化性能。
使用存儲過程
簡單的老的JDBC通過CallableStatement類支持存儲過程的調(diào)用。該類實(shí)際上是PreparedStatement的一個子類。假設(shè)我們 有一個poets數(shù)據(jù)庫。數(shù)據(jù)庫中有一個設(shè)置詩人逝世年齡的存儲過程。下面是對老酒鬼Dylan Thomas(old soak Dylan Thomas,不指定是否有關(guān)典故、文化,請批評指正。譯注)進(jìn)行調(diào)用的詳細(xì)代碼:
try
{
??? int age = 39;
??? String poetName = "dylan thomas";
??? CallableStatement proc =
??????? connection.prepareCall("{ call set_death_age(?, ?) }");
??? proc.setString(1, poetName);
??? proc.setInt(2, age);
??? cs.execute();
}
catch (SQLException e)
{
??? // ....
}
傳給prepareCall方法的字串是存儲過程調(diào)用的書寫規(guī)范。它指定了存儲過程的名稱,?代表了你需要指定的參數(shù)。?
和JDBC集成是存儲過程的一個很大的便利:為了從應(yīng)用中調(diào)用存儲過程,不需要存根(stub)類或者配置文件,除了你的DBMS的JDBC驅(qū)動程序外什么也不需要。?
當(dāng) 這段代碼執(zhí)行時,數(shù)據(jù)庫的存儲過程就被調(diào)用。我們沒有去獲取結(jié)果,因?yàn)樵摯鎯^程并不返回結(jié)果。執(zhí)行成功或失敗將通過例外得知。失敗可能意味著調(diào)用存儲過 程時的失敗(比如提供的一個參數(shù)的類型不正確),或者一個應(yīng)用程序的失敗(比如拋出一個例外指示在poets數(shù)據(jù)庫中并不存在“Dylan Thomas”)
結(jié)合SQL操作與存儲過程
映射Java對象到SQL表中的行相當(dāng)簡單,但是通常需要執(zhí)行幾個 SQL語句;可能是一個SELECT查找ID,然后一個INSERT插入指定ID的數(shù)據(jù)。在高度規(guī)格化(符合更高的范式,譯注)的數(shù)據(jù)庫模式中,可能需要 多個表的更新,因此需要更多的語句。Java代碼會很快地膨脹,每一個語句的網(wǎng)絡(luò)開銷也迅速增加。?
將這些SQL語句轉(zhuǎn)移到一個存儲過程中將大 大簡化代碼,僅涉及一次網(wǎng)絡(luò)調(diào)用。所有關(guān)聯(lián)的SQL操作都可以在數(shù)據(jù)庫內(nèi)部發(fā)生。并且,存儲過程語言,例如PL/SQL,允許使用SQL語法,這比 Java代碼更加自然。下面是我們早期的存儲過程,使用Oracle的PL/SQL語言編寫:
create procedure set_death_age(poet VARCHAR2, poet_age NUMBER)
??? poet_id NUMBER;
begin
SELECT id INTO poet_id FROM poets WHERE name = poet;
INSERT INTO deaths (mort_id, age) VALUES (poet_id, poet_age);
end set_death_age;
很獨(dú)特?不。我打賭你一定期待看到一個poets表上的UPDATE。這也暗示了使用存儲過程實(shí)現(xiàn)是多么容易的一件事情。set_death_age幾乎 可以肯定是一個很爛的實(shí)現(xiàn)。我們應(yīng)該在poets表中添加一列來存儲逝世年齡。Java代碼中并不關(guān)心數(shù)據(jù)庫模式是怎么實(shí)現(xiàn)的,因?yàn)樗鼉H調(diào)用存儲過程。我 們以后可以改變數(shù)據(jù)庫模式以提高性能,但是我們不必修改我們代碼。?
下面是調(diào)用上面存儲過程的Java代碼:
public static void setDeathAge(Poet dyingBard, int age)
??? throws SQLException
{
?? Connection con = null;
?? CallableStatement proc = null;
?? try
?? {
????? con = connectionPool.getConnection();
????? proc = con.prepareCall("{ call set_death_age(?, ?) }");
????? proc.setString(1, dyingBard.getName());
????? proc.setInt(2, age);
????? proc.execute();
?? }
?? finally
?? {
????? try
????? {
???????? proc.close();
????? }
????? catch (SQLException e) {}
????? con.close();
?? }
}
為了確??删S護(hù)性,建議使用像這兒這樣的static方法。這也使得調(diào)用存儲過程的代碼集中在一個簡單的模版代碼中。如果你用到許多存儲過程,就會發(fā)現(xiàn)僅需要拷貝、粘貼就可以創(chuàng)建新的方法。因?yàn)榇a的模版化,甚至也可以通過腳本自動生產(chǎn)調(diào)用存儲過程的代碼。
Functions
存儲過程可以有返回值,所以CallableStatement類有類似getResultSet這樣的方法來獲取返回值。當(dāng)存儲過程返回一個值時,你必 須使用registerOutParameter方法告訴JDBC驅(qū)動器該值的SQL類型是什么。你也必須調(diào)整存儲過程調(diào)用來指示該過程返回一個值。?
下面接著上面的例子。這次我們查詢Dylan Thomas逝世時的年齡。這次的存儲過程使用PostgreSQL的pl/pgsql:
create function snuffed_it_when (VARCHAR) returns integer '
declare
??? poet_id NUMBER;
??? poet_age NUMBER;
begin
??? -- first get the id associated with the poet.
??? SELECT id INTO poet_id FROM poets WHERE name = $1;
??? -- get and return the age.
??? SELECT age INTO poet_age FROM deaths WHERE mort_id = poet_id;
??? return age;
end;
' language 'pl/pgsql';
另外,注意pl/pgsql參數(shù)名通過Unix和DOS腳本的$n語法引用。同時,也注意嵌入的注釋,這是和Java代碼相比的另一個優(yōu)越性。在Java中寫這樣的注釋當(dāng)然是可以的,但是看起來很凌亂,并且和SQL語句脫節(jié),必須嵌入到Java String中。?
下面是調(diào)用這個存儲過程的Java代碼:
connection.setAutoCommit(false);
CallableStatement proc =
??? connection.prepareCall("{ ? = call snuffed_it_when(?) }");
proc.registerOutParameter(1, Types.INTEGER);
proc.setString(2, poetName);
cs.execute();
int age = proc.getInt(2);
如果指定了錯誤的返回值類型會怎樣?那么,當(dāng)調(diào)用存儲過程時將拋出一個RuntimeException,正如你在ResultSet操作中使用了一個錯誤的類型所碰到的一樣。
復(fù)雜的返回值
關(guān)于存儲過程的知識,很多人好像就熟悉我們所討論的這些。如果這是存儲過程的全部功能,那么存儲過程就不是其它遠(yuǎn)程執(zhí)行機(jī)制的替換方案了。存儲過程的功能比這強(qiáng)大得多。?
當(dāng)你執(zhí)行一個SQL查詢時,DBMS創(chuàng)建一個叫做cursor(游標(biāo))的數(shù)據(jù)庫對象,用于在返回結(jié)果中迭代每一行。ResultSet是當(dāng)前時間點(diǎn)的游標(biāo)的一個表示。這就是為什么沒有緩存或者特定數(shù)據(jù)庫的支持,你只能在ResultSet中向前移動。?
某些DBMS允許從存儲過程中返回游標(biāo)的一個引用。JDBC并不支持這個功能,但是Oracle、PostgreSQL和DB2的JDBC驅(qū)動器都支持在ResultSet上打開到游標(biāo)的指針(pointer)。?
設(shè)想列出所有沒有活到退休年齡的詩人,下面是完成這個功能的存儲過程,返回一個打開的游標(biāo),同樣也使用PostgreSQL的pl/pgsql語言:
create procedure list_early_deaths () return refcursor as '
declare
??? toesup refcursor;
begin
??? open toesup for
??????? SELECT poets.name, deaths.age
??????? FROM poets, deaths
??????? -- all entries in deaths are for poets.
??????? -- but the table might become generic.
??????? WHERE poets.id = deaths.mort_id
??????????? AND deaths.age < 60;
??? return toesup;
end;
' language 'plpgsql';
下面是調(diào)用該存儲過程的Java方法,將結(jié)果輸出到PrintWriter:?
PrintWriter:
static void sendEarlyDeaths(PrintWriter out)
{
??? Connection con = null;
??? CallableStatement toesUp = null;
??? try
??? {
??????? con = ConnectionPool.getConnection();
??????? // PostgreSQL needs a transaction to do this...
??????? con.setAutoCommit(false);
??????? // Setup the call.
??????? CallableStatement toesUp
??????????? = connection.prepareCall("{ ? = call list_early_deaths () }");
??????? toesUp.registerOutParameter(1, Types.OTHER);
??????? getResults.execute();
??????? ResultSet rs = (ResultSet) getResults.getObject(1);
??????? while (rs.next())
??????? {
??????????? String name = rs.getString(1);
??????????? int age = rs.getInt(2);
??????????? out.println(name + " was " + age + " years old.");
??????? }
??????? rs.close();
??? }
??? catch (SQLException e)
??? {
??????? // We should protect these calls.
??????? toesUp.close();
??????? con.close();
??? }
}
因?yàn)镴DBC并不直接支持從存儲過程中返回游標(biāo),我們使用Types.OTHER來指示存儲過程的返回類型,然后調(diào)用getObject()方法并對返回值進(jìn)行強(qiáng)制類型轉(zhuǎn)換。?
這個調(diào)用存儲過程的Java方法是mapping的一個好例子。Mapping是對一個集上的操作進(jìn)行抽象的方法。不是在這個過程上返回一個集,我們可以 把操作傳送進(jìn)去執(zhí)行。本例中,操作就是把ResultSet打印到一個輸出流。這是一個值得舉例的很常用的例子,下面是調(diào)用同一個存儲過程的另外一個方法 實(shí)現(xiàn):
public class ProcessPoetDeaths
{
??? public abstract void sendDeath(String name, int age);
}
static void mapEarlyDeaths(ProcessPoetDeaths mapper)
{
??? Connection con = null;
??? CallableStatement toesUp = null;
??? try
??? {
??????? con = ConnectionPool.getConnection();
??????? con.setAutoCommit(false);
??????? CallableStatement toesUp
??????????? = connection.prepareCall("{ ? = call list_early_deaths () }");
??????? toesUp.registerOutParameter(1, Types.OTHER);
??????? getResults.execute();
??????? ResultSet rs = (ResultSet) getResults.getObject(1);
??????? while (rs.next())
??????? {
??????????? String name = rs.getString(1);
??????????? int age = rs.getInt(2);
??????????? mapper.sendDeath(name, age);
??????? }
??????? rs.close();
??? }
??? catch (SQLException e)
??? {
??????? // We should protect these calls.
??????? toesUp.close();
??????? con.close();
??? }
}
這允許在ResultSet數(shù)據(jù)上執(zhí)行任意的處理,而不需要改變或者復(fù)制獲取ResultSet的方法:
static void sendEarlyDeaths(final PrintWriter out)
{
??? ProcessPoetDeaths myMapper = new ProcessPoetDeaths()
??? {
??????? public void sendDeath(String name, int age)
??????? {
??????????? out.println(name + " was " + age + " years old.");
??????? }
??? };
??? mapEarlyDeaths(myMapper);
}
這 個方法使用ProcessPoetDeaths的一個匿名實(shí)例調(diào)用mapEarlyDeaths。該實(shí)例擁有sendDeath方法的一個實(shí)現(xiàn),和我們上 面的例子一樣的方式把結(jié)果寫入到輸出流。當(dāng)然,這個技巧并不是存儲過程特有的,但是和存儲過程中返回的ResultSet結(jié)合使用,是一個非常強(qiáng)大的工 具。
結(jié)論
存儲過程可以幫助你在代碼中分離邏輯,這基本上總是有益的。這個分離的好處有:?
• 快速創(chuàng)建應(yīng)用,使用和應(yīng)用一起改變和改善的數(shù)據(jù)庫模式。?
• 數(shù)據(jù)庫模式可以在以后改變而不影響Java對象,當(dāng)我們完成應(yīng)用后,可以重新設(shè)計(jì)更好的模式。?
• 存儲過程通過更好的SQL嵌入使得復(fù)雜的SQL更容易理解。?
• 編寫存儲過程比在Java中編寫嵌入的SQL擁有更好的工具--大部分編輯器都提供語法高亮!?
• 存儲過程可以在任何SQL命令行中測試,這使得調(diào)試更加容易。
并不是所有的數(shù)據(jù)庫都支持存儲過程,但是存在許多很棒的實(shí)現(xiàn),包括免費(fèi)/開源的和非免費(fèi)的,所以移植并不是一個問題。Oracle、PostgreSQL和DB2都有類似的存儲過程語言,并且有在線的社區(qū)很好地支持。?
存儲過程工具很多,有像TOAD或TORA這樣的編輯器、調(diào)試器和IDE,提供了編寫、維護(hù)PL/SQL或pl/pgsql的強(qiáng)大的環(huán)境。?
存儲過程確實(shí)增加了你的代碼的開銷,但是它們和大多數(shù)的應(yīng)用服務(wù)器相比,開銷小得多。如果你的代碼復(fù)雜到需要使用DBMS,我建議整個采用存儲過程的方式。
總結(jié)
以上是生活随笔為你收集整理的callablestatement存储过程的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: JEECG ——11月份版本即将发布功能
- 下一篇: 程序员相亲记之准博士mm