熱點推薦:
您现在的位置: 電腦知識網 >> 編程 >> Java編程 >> Java核心技術 >> 正文

Java數據庫程序中的存儲過程設計

2013-11-23 18:52:17  來源: Java核心技術 

  本文闡述了怎麼使用DBMS存儲過程闡述了使用存儲過程的基本的和高級特性比如返回ResultSet本文假設你對DBMS和JDBC已經非常熟悉也假設你能夠毫無障礙地閱讀其它語言寫成的代碼(即不是Java的語言)但是並不要求你有任何存儲過程的編程經歷

  存儲過程是指保存在數據庫並在數據庫端執行的程序你可以使用特殊的語法在Java類中調用存儲過程在調用時存儲過程的名稱及指定的參數通過JDBC連接發送給DBMS執行存儲過程並通過連接(如果有)返回結果

  使用存儲過程擁有和使用基於EJB或CORBA這樣的應用服務器一樣的好處區別是存儲過程可以從很多流行的DBMS中免費使用而應用服務器大都非常昂貴這並不只是許可證費用的問題使用應用服務器所需要花費的管理編寫代碼的費用以及客戶程序所增加的復雜性都可以通過DBMS中的存儲過程所整個地替代

  你可以使用JavaPythonPerl或C編寫存儲過程但是通常使用你的DBMS所指定的特定語言Oracle使用PL/SQLPostgreSQL使用pl/pgsqlDB使用Procedural SQL這些語言都非常相似在它們之間移植存儲過程並不比在Sun的EJB規范不同實現版本之間移植Session Bean困難並且存儲過程是為嵌入SQL所設計這使得它們比Java或C等語言更加友好地方式表達數據庫的機制

  因為存儲過程運行在DBMS自身這可以幫助減少應用程序中的等待時間不是在Java代碼中執行個或個SQL語句而只需要在服務器端執行個存儲過程網絡上的數據往返次數的減少可以戲劇性地優化性能

  使用存儲過程

  簡單的老的JDBC通過CallableStatement類支持存儲過程的調用該類實際上是PreparedStatement的一個子類假設我們有一個poets數據庫數據庫中有一個設置詩人逝世年齡的存儲過程下面是對老酒鬼Dylan Thomas(old soak Dylan Thomas不指定是否有關典故文化請批評指正譯注)進行調用的詳細代碼

  

  try
{
 int age = ;
 String poetName = dylan thomas;
 CallableStatement proc = connectionprepareCall(
{ call set_death_age(? ?) });
 procsetString( poetName);
 procsetInt( age);
 csexecute();
}
catch (SQLException e)
{
 //
}

  傳給prepareCall方法的字串是存儲過程調用的書寫規范它指定了存儲過程的名稱?代表了你需要指定的參數

  和JDBC集成是存儲過程的一個很大的便利為了從應用中調用存儲過程不需要存根(stub)類或者配置文件除了你的DBMS的JDBC驅動程序外什麼也不需要

  當這段代碼執行時數據庫的存儲過程就被調用我們沒有去獲取結果因為該存儲過程並不返回結果執行成功或失敗將通過例外得知失敗可能意味著調用存儲過程時的失敗(比如提供的一個參數的類型不正確)或者一個應用程序的失敗(比如拋出一個例外指示在poets數據庫中並不存在Dylan Thomas

  結合SQL操作與存儲過程

  映射Java對象到SQL表中的行相當簡單但是通常需要執行幾個SQL語句可能是一個SELECT查找ID然後一個INSERT插入指定ID的數據在高度規格化(符合更高的范式譯注)的數據庫模式中可能需要多個表的更新因此需要更多的語句Java代碼會很快地膨脹每一個語句的網絡開銷也迅速增加

  將這些SQL語句轉移到一個存儲過程中將大大簡化代碼僅涉及一次網絡調用所有關聯的SQL操作都可以在數據庫內部發生並且存儲過程語言例如PL/SQL允許使用SQL語法這比Java代碼更加自然下面是我們早期的存儲過程使用Oracle的PL/SQL語言編寫

  

  create procedure set_death_age(poet VARCHAR 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;

  很獨特?不我打賭你一定期待看到一個poets表上的UPDATE這也暗示了使用存儲過程實現是多麼容易的一件事情set_death_age幾乎可以肯定是一個很爛的實現我們應該在poets表中添加一列來存儲逝世年齡Java代碼中並不關心數據庫模式是怎麼實現的因為它僅調用存儲過程我們以後可以改變數據庫模式以提高性能但是我們不必修改我們代碼

  下面是調用上面存儲過程的Java代碼

  

  

  public static void setDeathAge(Poet dyingBard int age)
throws SQLException
{
 Connection con = null;
 CallableStatement proc = null;

  try
 {
  con = connectionPoolgetConnection();
  proc = conprepareCall({ call set_death_age(? ?) });
  procsetString( dyingBardgetName());
  procsetInt( age);
  procexecute();
 }
 finally
 {
  try
  {
   procclose();
  }
  catch (SQLException e) {}
  conclose();
 }
}

  為了確保可維護性建議使用像這兒這樣的static方法這也使得調用存儲過程的代碼集中在一個簡單的模版代碼中如果你用到許多存儲過程就會發現僅需要拷貝粘貼就可以創建新的方法因為代碼的模版化甚至也可以通過腳本自動生產調用存儲過程的代碼

  Functions

  存儲過程可以有返回值所以CallableStatement類有類似getResultSet這樣的方法來獲取返回值當存儲過程返回一個值時你必須使用registerOutParameter方法告訴JDBC驅動器該值的SQL類型是什麼你也必須調整存儲過程調用來指示該過程返回一個值

  下面接著上面的例子這次我們查詢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 = $;
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參數名通過Unix和DOS腳本的$n語法引用同時也注意嵌入的注釋這是和Java代碼相比的另一個優越性在Java中寫這樣的注釋當然是可以的但是看起來很凌亂並且和SQL語句脫節必須嵌入到Java String中

  下面是調用這個存儲過程的Java代碼

  

  

  connectionsetAutoCommit(false);
CallableStatement proc =
connectionprepareCall({ ? = call snuffed_it_when(?) });
procregisterOutParameter( TypesINTEGER);
procsetString( poetName);
csexecute();
int age = procgetInt();

  如果指定了錯誤的返回值類型會怎樣?那麼當調用存儲過程時將拋出一個RuntimeException正如你在ResultSet操作中使用了一個錯誤的類型所碰到的一樣

  復雜的返回值

  關於存儲過程的知識很多人好像就熟悉我們所討論的這些如果這是存儲過程的全部功能那麼存儲過程就不是其它遠程執行機制的替換方案了存儲過程的功能比這強大得多

  當你執行一個SQL查詢時DBMS創建一個叫做cursor(游標)的數據庫對象用於在返回結果中迭代每一行ResultSet是當前時間點的游標的一個表示這就是為什麼沒有緩存或者特定數據庫的支持你只能在ResultSet中向前移動

  某些DBMS允許從存儲過程中返回游標的一個引用JDBC並不支持這個功能但是OraclePostgreSQL和DB的JDBC驅動器都支持在ResultSet上打開到游標的指針(pointer)

  設想列出所有沒有活到退休年齡的詩人下面是完成這個功能的存儲過程返回一個打開的游標同樣也使用PostgreSQL的pl/pgsql語言

  

  create procedure list_early_deaths () return refcursor as
declare
toesup refcursor;
begin
open toesup for
SELECT poetsname deathsage
FROM poets deaths
all entries in deaths are for poets
but the table might become generic
WHERE poetsid = deathsmort_id
AND deathsage < ;
return toesup;
end;
language plpgsql;

  下面是調用該存儲過程的Java方法將結果輸出到PrintWriter

  

  

  PrintWriter:

  static void sendEarlyDeaths(PrintWriter out)
{
 Connection con = null;
 CallableStatement toesUp = null;
 try
 {
  con = ConnectionPoolgetConnection();

  // PostgreSQL needs a transaction to do this
  consetAutoCommit(false);

  // Setup the call
  CallableStatement toesUp= connectionprepareCall(
{ ? = call list_early_deaths () });
  toesUpregisterOutParameter( TypesOTHER);
  getResultsexecute();

  ResultSet rs = (ResultSet) getResultsgetObject();
  while (rsnext())
  {
   String name = rsgetString();
   int age = rsgetInt();
   outprintln(name + was + age + years old);
  }
  rsclose();
 }
 catch (SQLException e)
 {
  // We should protect these calls
  toesUpclose();
  conclose();
 }
}

  因為JDBC並不直接支持從存儲過程中返回游標我們使用TypesOTHER來指示存儲過程的返回類型然後調用getObject()方法並對返回值進行強制類型轉換

  這個調用存儲過程的Java方法是mapping的一個好例子Mapping是對一個集上的操作進行抽象的方法不是在這個過程上返回一個集我們可以把操作傳送進去執行本例中操作就是把ResultSet打印到一個輸出流這是一個值得舉例的很常用的例子下面是調用同一個存儲過程的另外一個方法實現

  

  

  public class ProcessPoetDeaths
{
 public abstract void sendDeath(String name int age);
}

  static void mapEarlyDeaths(ProcessPoetDeaths mapper)
{
 Connection con = null;
 CallableStatement toesUp = null;
 try
 {
  con = ConnectionPoolgetConnection();
  consetAutoCommit(false);

  CallableStatement toesUp= connectionprepareCall({
? = call list_early_deaths () });
  toesUpregisterOutParameter( TypesOTHER);
  getResultsexecute();

  ResultSet rs = (ResultSet) getResultsgetObject();
  while (rsnext())
  {
   String name = rsgetString();
   int age = rsgetInt();
   mappersendDeath(name age);
  }
  rsclose();
 }
 catch (SQLException e)
 {
  // We should protect these calls
  toesUpclose();
  conclose();
 }
}

  這允許在ResultSet數據上執行任意的處理而不需要改變或者復制獲取ResultSet的方法

  

  static void sendEarlyDeaths(final PrintWriter out)
{
 ProcessPoetDeaths myMapper = new ProcessPoetDeaths()
 {
  public void sendDeath(String name int age)
  {
   outprintln(name + was + age + years old);
  }
 };
 mapEarlyDeaths(myMapper);
}

  這個方法使用ProcessPoetDeaths的一個匿名實例調用mapEarlyDeaths該實例擁有sendDeath方法的一個實現和我們上面的例子一樣的方式把結果寫入到輸出流當然這個技巧並不是存儲過程特有的但是和存儲過程中返回的ResultSet結合使用是一個非常強大的工具

  結論

  存儲過程可以幫助你在代碼中分離邏輯這基本上總是有益的這個分離的好處有

  快速創建應用使用和應用一起改變和改善的數據庫模式

  數據庫模式可以在以後改變而不影響Java對象當我們完成應用後可以重新設計更好的模式

  存儲過程通過更好的SQL嵌入使得復雜的SQL更容易理解

  編寫存儲過程比在Java中編寫嵌入的SQL擁有更好的工具——大部分編輯器都提供語法高亮!

  存儲過程可以在任何SQL命令行中測試這使得調試更加容易

  並不是所有的數據庫都支持存儲過程但是存在許多很棒的實現包括免費/開源的和非免費的所以移植並不是一個問題OraclePostgreSQL和DB都有類似的存儲過程語言並且有在線的社區很好地支持

  存儲過程工具很多有像TOAD或TORA這樣的編輯器調試器和IDE提供了編寫維護PL/SQL或pl/pgsql的強大的環境

  存儲過程確實增加了你的代碼的開銷但是它們和大多數的應用服務器相比開銷小得多如果你的代碼復雜到需要使用DBMS我建議整個采用存儲過程的方式


From:http://tw.wingwit.com/Article/program/Java/hx/201311/25906.html
    推薦文章
    Copyright © 2005-2013 電腦知識網 Computer Knowledge   All rights reserved.