預存程序是儲存在資料庫端執行的程式碼單元。相較於 EJB 或 CORBA 等應用伺服器方案,預存程序具備顯著優勢:
JDBC 透過 CallableStatement(繼承自 PreparedStatement)支援預存程序呼叫。標準呼叫語法:{ call procedure_name(?, ?) },問號代表參數佔位符。
try {
int age = 39;
String poetName = "dylan thomas";
CallableStatement proc = connection.prepareCall("{ call set_death_age(?, ?) }");
proc.setString(1, poetName);
proc.setInt(2, age);
proc.execute();
} catch (SQLException e) {
// 錯誤處理:參數型別錯誤、資料不存在等
}
關鍵優勢:無需 Stub 類別、無需配置檔,僅需 JDBC 驅動程式即可完成呼叫。
高度正規化資料庫常需多表操作(查詢 ID → 插入關聯表)。將邏輯移入預存程序可實現:
create procedure set_death_age(poet VARCHAR2, poet_age NUMBER) as
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;
架構優勢:Java 端完全屏蔽資料庫結構變更。若未來在 poets 表新增 death_age 欄位優化查詢,僅需修改預存程序,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 { if (proc != null) proc.close(); } catch (SQLException ignore) {}
if (con != null) con.close();
}
}
建議採用 static 方法集中管理呼叫模板,甚至可透過腳本自動生成呼叫程式碼,確保可維護性。
預存函數可返回單一值,呼叫語法調整為 { ? = call function_name(?) },需預先註冊輸出參數型別:
connection.setAutoCommit(false);
CallableStatement proc = connection.prepareCall("{ ? = call snuffed_it_when(?) }");
proc.registerOutParameter(1, Types.INTEGER);
proc.setString(2, poetName);
proc.execute();
int age = proc.getInt(1); // 注意:輸出參數索引從 1 開始
型別不匹配將拋出 RuntimeException,同 ResultSet 型別錯誤行為一致。
此為預存程序替代應用伺服器的關鍵能力。部分 DBMS(Oracle、PostgreSQL、DB2)支援從預存程序返回游標引用,JDBC 驅動提供 ResultSet 包裝。
create procedure list_early_deaths() return refcursor as $$
declare
toesup refcursor;
begin
open toesup for
SELECT poets.name, deaths.age
FROM poets, deaths
WHERE poets.id = deaths.mort_id AND deaths.age < 60;
return toesup;
end; $$ language 'plpgsql';
static void sendEarlyDeaths(PrintWriter out) {
Connection con = null;
CallableStatement toesUp = null;
try {
con = ConnectionPool.getConnection();
con.setAutoCommit(false); // PostgreSQL 需顯式事務
toesUp = con.prepareCall("{ ? = call list_early_deaths() }");
toesUp.registerOutParameter(1, Types.OTHER); // 關鍵:使用 OTHER 型別
toesUp.execute();
ResultSet rs = (ResultSet) toesUp.getObject(1); // 強制轉型
while (rs.next()) {
out.println(rs.getString(1) + " was " + rs.getInt(2) + " years old.");
}
rs.close();
} catch (SQLException e) {
// 資源釋放
}
}
定義介面 ProcessPoetDeaths,實現 sendDeath 方法,透過匿名類別注入具體行為:
public interface ProcessPoetDeaths {
void sendDeath(String name, int age);
}
static void mapEarlyDeaths(ProcessPoetDeaths mapper) {
// ... 獲取 ResultSet 同前 ...
while (rs.next()) {
mapper.sendDeath(rs.getString(1), rs.getInt(2));
}
}
// 使用時注入具體行為
static void sendEarlyDeaths(final PrintWriter out) {
mapEarlyDeaths(new ProcessPoetDeaths() {
public void sendDeath(String name, int age) {
out.println(name + " was " + age + " years old.");
}
});
}
此模式將「資料獲取」與「資料處理」完全解耦,單一預存程序可支援多種下游處理邏輯(列印、報表、快取、轉檔等),極大提升程式碼複用率。
雖預存程序增加部署複雜度,但相較應用伺服器架構,其總擁有成本(TCO)顯著更低。Oracle、PostgreSQL、DB2 擁有相似的程序語言與活躍社群,移植風險可控。對於資料密集型、高併發、需長期演進的企業級系統,預存程序是極具策略價值的架構選擇。