Oracle 資料庫的 PL/SQL 存儲過程憑藉強大的過程化能力、就近處理資料減少網路傳輸、執行計劃緩存復用、安全權限控制等優勢,長期佔據企業級核心業務邏輯的主導地位。JSP/Servlet 應用作為展示層,高效、安全地調用存儲過程是必備技能。本文系統講解參數綁定、結果集處理、異常傳播、效能優化全流程。
| 模式 | 關鍵字 | 數據流向 | JDBC 對應 | 典型場景 |
|---|---|---|---|---|
| IN | IN / 默認 | Java → DB | setXxx() | 查詢條件、插入值 |
| OUT | OUT | DB → Java | registerOutParameter() + getXxx() | 返回狀態碼、自增 ID、計算結果 |
| IN OUT | IN OUT | 雙向 | setXxx() + registerOutParameter() + getXxx() | 累加器、雙向交換數據 |
| REF CURSOR | SYS_REFCURSOR | DB → Java (ResultSet) | registerOutParameter(Types.REF_CURSOR) + getObject() | 返回多行查詢結果 |
CREATE OR REPLACE PROCEDURE p_insert_log (
p_module IN VARCHAR2,
p_level IN VARCHAR2,
p_message IN VARCHAR2,
p_log_id OUT NUMBER
) AS
BEGIN
INSERT INTO sys_log (module, level, message, create_time)
VALUES (p_module, p_level, p_message, SYSTIMESTAMP)
RETURNING log_id INTO p_log_id; -- 直接返回自增值,無需額外查詢
COMMIT;
EXCEPTION
WHEN OTHERS THEN
ROLLBACK;
RAISE; -- 向上拋出異常,JDBC 可捕獲 SQLException
END;
/
<%@ page import="java.sql.*, oracle.jdbc.*" %>
<%
String url = "jdbc:oracle:thin:@//192.168.1.100:1521/ORCL";
String user = "app_user", pwd = "secure_pwd";
try (Connection conn = DriverManager.getConnection(url, user, pwd);
CallableStatement cstmt = conn.prepareCall("{call p_insert_log(?, ?, ?, ?)}")) {
cstmt.setString(1, "OrderService");
cstmt.setString(2, "ERROR");
cstmt.setString(3, "訂單支付回調失敗:簽名驗證不通過");
cstmt.registerOutParameter(4, Types.NUMERIC); // OUT 參數
cstmt.executeUpdate(); // 執行 DML 過程用 executeUpdate
long newLogId = cstmt.getLong(4);
out.println("日誌記錄成功,ID: " + newLogId);
} catch (SQLException e) {
out.println("調用失敗: " + e.getMessage());
e.printStackTrace();
}
%>
此為報表、列表頁最常見場景,存儲過程打開游標,Java 端獲取 ResultSet 迭代渲染。
CREATE OR REPLACE PACKAGE pkg_report AS
TYPE ref_cursor IS REF CURSOR;
PROCEDURE get_user_orders (
p_user_id IN NUMBER,
p_start_dt IN DATE,
p_end_dt IN DATE,
p_cursor OUT ref_cursor
);
END pkg_report;
/
CREATE OR REPLACE PACKAGE BODY pkg_report AS
PROCEDURE get_user_orders (
p_user_id IN NUMBER,
p_start_dt IN DATE,
p_end_dt IN DATE,
p_cursor OUT ref_cursor
) AS
BEGIN
OPEN p_cursor FOR
SELECT o.order_id, o.order_no, o.total_amount, o.status, o.create_time
FROM orders o
WHERE o.user_id = p_user_id
AND o.create_time BETWEEN p_start_dt AND p_end_dt
ORDER BY o.create_time DESC;
END;
END pkg_report;
/
<%@ page import="java.sql.*, oracle.jdbc.*" %>
<%
long userId = Long.parseLong(request.getParameter("userId"));
java.sql.Date start = java.sql.Date.valueOf(request.getParameter("start"));
java.sql.Date end = java.sql.Date.valueOf(request.getParameter("end"));
String sql = "{call pkg_report.get_user_orders(?, ?, ?, ?)}";
try (Connection conn = dataSource.getConnection();
CallableStatement cstmt = conn.prepareCall(sql)) {
cstmt.setLong(1, userId);
cstmt.setDate(2, start);
cstmt.setDate(3, end);
cstmt.registerOutParameter(4, OracleTypes.CURSOR); // 關鍵:OracleTypes.CURSOR
cstmt.execute();
// 獲取 REF CURSOR 轉為 ResultSet
try (ResultSet rs = (ResultSet) cstmt.getObject(4)) {
out.println("<table border='1' cellpadding='5'>");
out.println("<tr><th>訂單號</th><th>金額</th><th>狀態</th><th>下單時間</th></tr>");
while (rs.next()) {
out.println("<tr>");
out.println("<td>" + rs.getString("order_no") + "</td>");
out.println("<td>" + rs.getBigDecimal("total_amount") + "</td>");
out.println("<td>" + rs.getString("status") + "</td>");
out.println("<td>" + rs.getTimestamp("create_time") + "</td>");
out.println("</tr>");
}
out.println("</table>");
}
} catch (SQLException e) {
out.println("查詢異常: " + e.getMessage());
}
%>
Oracle 支援傳遞集合(VARRAY/NESTED TABLE)與物件類型,對應 JDBC ARRAY、STRUCT,需創建對應的 Java 類實現 SQLData 或 ORAData 介面。現代應用建議改用 JSON 字串傳遞複雜結構,或在中間層組裝,降低資料庫耦合度。
dataSourceClassName=oracle.jdbc.pool.OracleDataSource,啟用 implicitCachingEnabled=true 緩存 PreparedStatement/CallableStatement。OraclePreparedStatement.setExecuteBatch() 或標準 addBatch()。getCharacterStream()/getBinaryStream() 流式讀取,避免記憶體溢出。ORA-01000: maximum open cursors exceeded)。RAISE_APPLICATION_ERROR(-20001, '業務錯誤') 拋出自定義錯誤碼,Java 端捕獲 SQLException.getErrorCode() 進行業務判斷。connectionProperties.setProperty("oracle.jdbc.implicitStatementCacheSize", "100")。PLS-00306: wrong number or types of arguments:檢查參數順序、個數、類型映射(特別是 DATE/TIMESTAMP、NUMBER 對應 Java 類型)。ORA-06550: line 1, column 7: PLS-00905: object is invalid:存儲過程編譯失效,需重新編譯 ALTER PROCEDURE ... COMPILE。java.sql.SQLException: Invalid column type: 2012:REF CURSOR 註冊類型錯誤,必須用 OracleTypes.CURSOR (-10) 或 Types.REF_CURSOR (2012,JDBC 4.2+)。Connection/CallableStatement/ResultSet 在 finally 或 try-with-resources 中關閉。雖然 Oracle 存儲過程強大,但不建議將所有業務邏輯下沉:
採用「厚服務層、薄資料層」架構,業務邏輯留在 Java/Spring 服務層,資料庫專注資料存取與集合運算,利用 ORM(MyBatis/JPA)管理 SQL,存儲過程作為效能優化的最後手段而非首選。
JSP 調用 Oracle 存儲過程的核心在於:正確映射參數模式(IN/OUT/IN OUT/REF CURSOR)、使用 CallableStatement 配合 OracleTypes、嚴格管理資源生命週期、善用連接池語句緩存。掌握 REF CURSOR 返回結果集、自定義錯誤碼傳遞、批量綁定等進階技巧,能顯著提升企業級應用的開發效率與運行穩定性。