JDBC 使用統一逸出語法呼叫預存程序,驅動程式轉換為目標資料庫原生呼叫格式:
// 無回傳值
{call procedure_name(?, ?, ...)}
// 有回傳值 (函數)
{? = call function_name(?, ?, ...)}
// 無參數
{call procedure_name}
Connection conn = dataSource.getConnection();
// 兩個參數,無回傳值
CallableStatement cstmt = conn.prepareCall("{call get_employee_details(?, ?)}");
cstmt.setString(1, "EMP001");
cstmt.setDate(2, Date.valueOf("2024-01-01"));
// 註冊 OUT 參數型別 (對應資料庫定義)
cstmt.registerOutParameter(1, Types.VARCHAR); // 字串
cstmt.registerOutParameter(2, Types.INTEGER); // 整數
cstmt.registerOutParameter(3, Types.DECIMAL, 2); // 數值,小數位數 2
cstmt.registerOutParameter(4, Types.TIMESTAMP); // 日期時間
cstmt.registerOutParameter(5, Types.BLOB); // 大物件
cstmt.execute(); // 執行
// 取得值 (對應 Java 型別)
String name = cstmt.getString(1);
int count = cstmt.getInt(2);
BigDecimal salary = cstmt.getBigDecimal(3, 2);
Timestamp hired = cstmt.getTimestamp(4);
Blob photo = cstmt.getBlob(5);
// 假設預存程序:INOUT bonus_factor DECIMAL(5,2)
// 1. 設定 IN 值
cstmt.setBigDecimal(1, new BigDecimal("1.50"));
// 2. 註冊 OUT 型別 (必須與 IN 型別相容)
cstmt.registerOutParameter(1, Types.DECIMAL, 2);
// 3. 執行
cstmt.executeUpdate(); // 或 execute()
// 4. 取得更新後的 OUT 值
BigDecimal newFactor = cstmt.getBigDecimal(1, 2);
| JDBC Type (Types.xxx) | registerOutParameter | 對應 getXXX 方法 | Java 型別 |
|---|---|---|---|
| BIT / BOOLEAN | registerOutParameter(i, Types.BOOLEAN) | getBoolean(i) | boolean |
| TINYINT | registerOutParameter(i, Types.TINYINT) | getByte(i) | byte |
| SMALLINT | registerOutParameter(i, Types.SMALLINT) | getShort(i) | short |
| INTEGER | registerOutParameter(i, Types.INTEGER) | getInt(i) | int |
| BIGINT | registerOutParameter(i, Types.BIGINT) | getLong(i) | long |
| REAL | registerOutParameter(i, Types.REAL) | getFloat(i) | float |
| DOUBLE / FLOAT | registerOutParameter(i, Types.DOUBLE) | getDouble(i) | double |
| DECIMAL / NUMERIC | registerOutParameter(i, Types.DECIMAL, scale) | getBigDecimal(i, scale) | BigDecimal |
| CHAR / VARCHAR / LONGVARCHAR | registerOutParameter(i, Types.VARCHAR) | getString(i) | String |
| BINARY / VARBINARY / LONGVARBINARY | registerOutParameter(i, Types.VARBINARY) | getBytes(i) | byte[] |
| DATE | registerOutParameter(i, Types.DATE) | getDate(i) | java.sql.Date |
| TIME | registerOutParameter(i, Types.TIME) | getTime(i) | java.sql.Time |
| TIMESTAMP | registerOutParameter(i, Types.TIMESTAMP) | getTimestamp(i) | java.sql.Timestamp |
| BLOB | registerOutParameter(i, Types.BLOB) | getBlob(i) | Blob |
| CLOB | registerOutParameter(i, Types.CLOB) | getClob(i) | Clob |
| ARRAY | registerOutParameter(i, Types.ARRAY) | getArray(i) | Array |
| REF | registerOutParameter(i, Types.REF) | getRef(i) | Ref |
| STRUCT | registerOutParameter(i, Types.STRUCT) | getObject(i) | Struct |
CREATE OR REPLACE PROCEDURE adjust_salary (
p_emp_id IN employees.employee_id%TYPE,
p_pct IN NUMBER, -- 調整百分比 (如 0.1 = 10%)
p_old_sal OUT employees.salary%TYPE,
p_new_sal OUT employees.salary%TYPE,
p_status OUT VARCHAR2
) AS
v_current_sal employees.salary%TYPE;
BEGIN
SELECT salary INTO v_current_sal FROM employees WHERE employee_id = p_emp_id;
p_old_sal := v_current_sal;
p_new_sal := v_current_sal * (1 + p_pct);
UPDATE employees SET salary = p_new_sal WHERE employee_id = p_emp_id;
p_status := 'SUCCESS';
COMMIT;
EXCEPTION
WHEN NO_DATA_FOUND THEN
p_status := 'EMP_NOT_FOUND';
p_old_sal := 0;
p_new_sal := 0;
WHEN OTHERS THEN
p_status := 'ERROR: ' || SQLERRM;
ROLLBACK;
END;
public class SalaryAdjustService {
public AdjustResult adjustSalary(long empId, double pct) {
String sql = "{call adjust_salary(?, ?, ?, ?, ?)}";
try (Connection conn = dataSource.getConnection();
CallableStatement cstmt = conn.prepareCall(sql)) {
// IN 參數
cstmt.setLong(1, empId);
cstmt.setDouble(2, pct);
// OUT 參數註冊
cstmt.registerOutParameter(3, Types.DECIMAL, 2); // old_sal
cstmt.registerOutParameter(4, Types.DECIMAL, 2); // new_sal
cstmt.registerOutParameter(5, Types.VARCHAR); // status
// 執行
cstmt.executeUpdate();
// 取得 OUT 參數
BigDecimal oldSal = cstmt.getBigDecimal(3, 2);
BigDecimal newSal = cstmt.getBigDecimal(4, 2);
String status = cstmt.getString(5);
return new AdjustResult(empId, oldSal, newSal, status);
} catch (SQLException e) {
throw new RuntimeException("Salary adjust failed", e);
}
}
static class AdjustResult {
final long empId;
final BigDecimal oldSalary, newSalary;
final String status;
// constructor, getters, toString...
}
}
鐵律:先遍歷所有 ResultSet (含 getMoreResults() 迴圈),最後 才讀取 OUT 參數。否則某些驅動 (Oracle, SQL Server) 會丟失 OUT 參數值。
// OUT 參數可能是 NULL
BigDecimal bonus = cstmt.getBigDecimal(1, 2);
if (cstmt.wasNull()) {
bonus = BigDecimal.ZERO; // 業務預設
}
// Oracle 特有:預存程序返回 SYS_REFCURSOR
// SQL: {call get_employees_by_dept(?, ?)}
cstmt.registerOutParameter(1, OracleTypes.CURSOR); // 需 oracle.jdbc.OracleTypes
cstmt.setInt(2, deptId);
cstmt.execute();
ResultSet rs = (ResultSet) cstmt.getObject(1); // 強轉
while (rs.next()) { /* ... */ }
rs.close(); // 記得關閉!
| 資料庫 | 呼叫語法 | OUT 參數註冊 | 游標支援 |
|---|---|---|---|
| Oracle | {call proc(?,?)} | 標準 JDBC + OracleTypes.CURSOR | REF CURSOR 強轉 ResultSet |
| PostgreSQL | {? = call func(?,?)} | 標準 JDBC | REFCURSOR → getObject → ResultSet |
| MySQL | {call proc(?,?)} | 標準 JDBC | 不支援 OUT 游標 (用結果集) |
| SQL Server | {? = call proc(?,?)} | 標準 JDBC | OUTPUT 參數 / 結果集 |
CallableStatement 是 JDBC 最複雜的介面。實戰中請封裝成 DAO 方法,隱藏參數註冊、型別轉換、異常處理細節,上層業務碼只見領域物件輸入輸出。