首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> Java 程式設計 >> Java 相關知識

JDBC 進階教學:CallableStatement 呼叫預存程序完整實戰

     編輯:Java 相關知識

預存程序呼叫標準語法

JDBC 使用統一逸出語法呼叫預存程序,驅動程式轉換為目標資料庫原生呼叫格式:

// 無回傳值
{call procedure_name(?, ?, ...)}

// 有回傳值 (函數)
{? = call function_name(?, ?, ...)}

// 無參數
{call procedure_name}

建立 CallableStatement

Connection conn = dataSource.getConnection();
// 兩個參數,無回傳值
CallableStatement cstmt = conn.prepareCall("{call get_employee_details(?, ?)}");

參數模式深度解析

1. IN 參數:同 PreparedStatement

cstmt.setString(1, "EMP001");
cstmt.setDate(2, Date.valueOf("2024-01-01"));

2. OUT 參數:必須先註冊 JDBC 型別

// 註冊 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);

3. INOUT 參數:雙向傳遞

// 假設預存程序: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 型別與 Java 型別標準映射表 (registerOutParameter → getXXX)

JDBC Type (Types.xxx)registerOutParameter對應 getXXX 方法Java 型別
BIT / BOOLEANregisterOutParameter(i, Types.BOOLEAN)getBoolean(i)boolean
TINYINTregisterOutParameter(i, Types.TINYINT)getByte(i)byte
SMALLINTregisterOutParameter(i, Types.SMALLINT)getShort(i)short
INTEGERregisterOutParameter(i, Types.INTEGER)getInt(i)int
BIGINTregisterOutParameter(i, Types.BIGINT)getLong(i)long
REALregisterOutParameter(i, Types.REAL)getFloat(i)float
DOUBLE / FLOATregisterOutParameter(i, Types.DOUBLE)getDouble(i)double
DECIMAL / NUMERICregisterOutParameter(i, Types.DECIMAL, scale)getBigDecimal(i, scale)BigDecimal
CHAR / VARCHAR / LONGVARCHARregisterOutParameter(i, Types.VARCHAR)getString(i)String
BINARY / VARBINARY / LONGVARBINARYregisterOutParameter(i, Types.VARBINARY)getBytes(i)byte[]
DATEregisterOutParameter(i, Types.DATE)getDate(i)java.sql.Date
TIMEregisterOutParameter(i, Types.TIME)getTime(i)java.sql.Time
TIMESTAMPregisterOutParameter(i, Types.TIMESTAMP)getTimestamp(i)java.sql.Timestamp
BLOBregisterOutParameter(i, Types.BLOB)getBlob(i)Blob
CLOBregisterOutParameter(i, Types.CLOB)getClob(i)Clob
ARRAYregisterOutParameter(i, Types.ARRAY)getArray(i)Array
REFregisterOutParameter(i, Types.REF)getRef(i)Ref
STRUCTregisterOutParameter(i, Types.STRUCT)getObject(i)Struct

完整實戰範例:員工薪資調整預存程序

資料庫端 (Oracle PL/SQL 範例)

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;

Java 呼叫端

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...
    }
}

進階主題與避坑指南

1. 結果集 + OUT 參數混合處理順序

鐵律:先遍歷所有 ResultSet (含 getMoreResults() 迴圈),最後 才讀取 OUT 參數。否則某些驅動 (Oracle, SQL Server) 會丟失 OUT 參數值。

2. NULL 值處理

// OUT 參數可能是 NULL
BigDecimal bonus = cstmt.getBigDecimal(1, 2);
if (cstmt.wasNull()) {
    bonus = BigDecimal.ZERO; // 業務預設
}

3. 游標回傳 (Oracle REF CURSOR)

// 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(); // 記得關閉!

4. 效能與連線池考量

  • 預存程序在資料庫端執行,減少網路往返,適合複雜業務邏輯、大量資料處理。
  • 但業務邏輯下沉資料庫會導致版本控制困難、測試不便、資料庫鎖定風險。現代架構傾向業務邏輯留在應用層,預存程序僅用於報表統計、資料遷移、跨資料庫同步等邊緣場景。
  • 呼叫預存程序時,連線持有時間較長,連線池大小需評估併發度。

5. 資料庫差異備忘錄

資料庫呼叫語法OUT 參數註冊游標支援
Oracle{call proc(?,?)}標準 JDBC + OracleTypes.CURSORREF CURSOR 強轉 ResultSet
PostgreSQL{? = call func(?,?)}標準 JDBCREFCURSOR → getObject → ResultSet
MySQL{call proc(?,?)}標準 JDBC不支援 OUT 游標 (用結果集)
SQL Server{? = call proc(?,?)}標準 JDBCOUTPUT 參數 / 結果集

CallableStatement 是 JDBC 最複雜的介面。實戰中請封裝成 DAO 方法,隱藏參數註冊、型別轉換、異常處理細節,上層業務碼只見領域物件輸入輸出。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved