在企業級應用中,將複雜業務邏輯下沉到數據庫存儲過程,能減少網絡交互、利用數據庫引擎優化、封裝核心算法。JDBC 提供 CallableStatement 專門用於調用存儲過程。同時,JDBC 定義了 SQL Escape 語法,屏蔽不同數據庫在日期時間、標量函數、外連接、存儲過程調用等方面的方言差異,實現「寫一次,跑多庫」。
DELIMITER $$
CREATE PROCEDURE `get_emp_name`(
IN p_emp_id INT,
OUT p_emp_name VARCHAR(100)
)
BEGIN
SELECT first_name INTO p_emp_name FROM employees WHERE id = p_emp_id;
END$$
DELIMITER ;
| 模式 | 說明 | JDBC 方法 |
|---|---|---|
| IN | 輸入參數,調用前綁定值 | setXxx(index, value) |
| OUT | 輸出參數,調用後獲取值 | registerOutParameter(index, sqlType) → getXxx(index) |
| INOUT | 雙向參數,調用前綁定、調用後獲取 | setXxx + registerOutParameter + getXxx |
public String getEmployeeName(Connection conn, int empId) throws SQLException {
String sql = "{call get_emp_name(?, ?)}"; // 標準 Escape 語法
try (CallableStatement cstmt = conn.prepareCall(sql)) {
// 1. 綁定 IN 參數
cstmt.setInt(1, empId);
// 2. 註冊 OUT 參數類型
cstmt.registerOutParameter(2, Types.VARCHAR);
// 3. 執行
cstmt.execute();
// 4. 獲取 OUT 參數
return cstmt.getString(2);
} // 自動關閉 cstmt
}
getResultSet() → 處理 → getMoreResults() 循環獲取。conn.setAutoCommit(false),調用後 commit/rollback。Escape 語法格式:{keyword 'parameters'}。驅動程序解析後翻譯為目標數據庫原生語法。
d、t、ts| 關鍵字 | 格式 | 示例 | 對應類型 |
|---|---|---|---|
d | yyyy-MM-dd | {d '2024-03-15'} | DATE |
t | HH:mm:ss | {t '14:30:00'} | TIME |
ts | yyyy-MM-dd HH:mm:ss | {ts '2024-03-15 14:30:00'} | TIMESTAMP |
// 插入日期示例
String sql = "INSERT INTO events (id, name, event_date) VALUES (1, 'Launch', {d '2024-01-01'})";
stmt.executeUpdate(sql);
fn調用數據庫內置函數(如字符串、數學、轉換函數),驅動自動映射到目標 DB 函數名。
// 字符串長度
{fn length('Hello World')} -- 返回 11
-- MySQL: LENGTH(), Oracle: LENGTH(), SQL Server: LEN()
-- 大小寫轉換
{fn upper('abc')} -- 'ABC'
{fn lower('ABC')} -- 'abc'
-- 截取子串(注意:參數從 1 開始)
{fn substring('Hello', 2, 3)} -- 'ell'
-- 當前時間(數據庫服務端時間)
{fn now()} 或 {fn curdate()} / {fn curtime()}
escape-- 查詢包含字面量 '%' 的字符串,指定 '\' 為轉義字符
SELECT * FROM symbols WHERE symbol LIKE '\%' {escape '\'}
-- Java 字符串中需雙重轉義
String sql = "SELECT * FROM symbols WHERE symbol LIKE '\\\\%' {escape '\\\\'}";
call-- 無返回值
{call my_proc(?, ?)}
-- 有返回值(函數風格)
{? = call my_func(?, ?)}
oj(舊語法,現代建議直接寫 ANSI SQL){oj table1 LEFT OUTER JOIN table2 ON table1.id = table2.fk}
-- 等價於標準 SQL
SELECT * FROM table1 LEFT JOIN table2 ON table1.id = table2.fk
public void batchUpdateSalaries(Connection conn, List<SalaryAdjustment> adjustments) throws SQLException {
String sql = "{call adjust_salary(?, ?, ?)}"; // IN emp_id, IN percent, OUT new_salary
conn.setAutoCommit(false);
try (CallableStatement cstmt = conn.prepareCall(sql)) {
for (SalaryAdjustment adj : adjustments) {
cstmt.setInt(1, adj.getEmpId());
cstmt.setDouble(2, adj.getPercent());
cstmt.registerOutParameter(3, Types.DECIMAL);
cstmt.addBatch(); // 批量處理
}
int[] results = cstmt.executeBatch(); // 執行批量
for (int i = 0; i < results.length; i++) {
if (results[i] == Statement.SUCCESS_NO_INFO) {
BigDecimal newSal = cstmt.getBigDecimal(3);
adjustments.get(i).setNewSalary(newSal);
}
}
conn.commit();
} catch (BatchUpdateException e) {
conn.rollback();
throw e;
} finally {
conn.setAutoCommit(true);
}
}
registerOutParameter 的 SQL 類型(Types.VARCHAR 等)必須與數據庫定義一致,否則獲取時可能報錯或數據截斷。SYS_REFCURSOR 返回結果集,需註冊為 OracleTypes.CURSOR,獲取後轉型 ResultSet 處理。CallableStatement 是 JDBC 調用存儲過程的標準接口,配合 IN/OUT/INOUT 參數模式實現雙向數據交換。JDBC Escape 語法為日期時間、標量函數、存儲過程調用提供了跨數據庫的統一寫法,降低遷移成本。實戰中務必注意資源關閉(try-with-resources)、事務邊界、批量執行優化,以及驅動對特定 Escape 關鍵字的支持程度。