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

Java 調用 MySQL 預存過程完整指南:參數綁定、結果集處理與最佳實踐

     編輯:Java 相關知識

預存過程基礎概念

預存過程是存儲在數據庫中的預編譯 SQL 程序,優點:

  • 減少網絡傳輸(批量操作單次調用)
  • 服務端執行,性能優於客戶端拼接 SQL
  • 安全性:防 SQL 注入、權限控制
  • 業務邏輯下沉,便於維護

MySQL 預存過程示例

DELIMITER //
CREATE PROCEDURE sp_emps_in_dept(IN dept_id INT)
BEGIN
    -- 結果集 1:部門信息
    SELECT department_name, location 
    FROM departments WHERE department_id = dept_id;
    
    -- 結果集 2:員工列表
    SELECT employee_id, first_name, last_name 
    FROM employees WHERE department_id = dept_id;
END //
DELIMITER ;

Java 調用核心代碼

import java.sql.*;
import java.util.ArrayList;
import java.util.List;

public class StoredProcedureCaller {
    
    public void callEmpsInDept(Connection conn, int deptId) throws SQLException {
        String sql = "{CALL sp_emps_in_dept(?)}";
        
        try (CallableStatement cstmt = conn.prepareCall(sql)) {
            // 1. 綁定輸入參數
            cstmt.setInt(1, deptId);
            
            // 2. 執行
            boolean hasResults = cstmt.execute();
            
            // 3. 處理多結果集
            int resultSetIndex = 0;
            do {
                if (hasResults) {
                    try (ResultSet rs = cstmt.getResultSet()) {
                        resultSetIndex++;
                        System.out.println("=== 結果集 " + resultSetIndex + " ===");
                        processResultSet(rs, resultSetIndex);
                    }
                }
                // 獲取下一個結果集
                hasResults = cstmt.getMoreResults();
            } while (hasResults || cstmt.getUpdateCount() != -1);
            
            // 4. 獲取輸出參數(如有)
            // cstmt.registerOutParameter(2, Types.INTEGER); // 需預先註冊
            // int outParam = cstmt.getInt(2);
        }
    }
    
    private void processResultSet(ResultSet rs, int index) throws SQLException {
        ResultSetMetaData meta = rs.getMetaData();
        int columnCount = meta.getColumnCount();
        
        // 打印列名
        for (int i = 1; i <= columnCount; i++) {
            System.out.printf("%-20s", meta.getColumnLabel(i));
        }
        System.out.println();
        
        // 打印數據
        while (rs.next()) {
            for (int i = 1; i <= columnCount; i++) {
                System.out.printf("%-20s", rs.getObject(i));
            }
            System.out.println();
        }
    }
}

參數類型完整映射

MySQL 類型JDBC TypesJava 類型Setter/Getter
INTTypes.INTEGERint / IntegersetInt / getInt
BIGINTTypes.BIGINTlong / LongsetLong / getLong
VARCHARTypes.VARCHARStringsetString / getString
DECIMALTypes.DECIMALBigDecimalsetBigDecimal / getBigDecimal
DATETypes.DATEjava.sql.DatesetDate / getDate
DATETIMETypes.TIMESTAMPLocalDateTime / TimestampsetObject / getObject
JSONTypes.OTHERString / JsonNodesetString / getString

輸出參數與 INOUT 參數處理

-- MySQL 預存過程帶輸出參數
CREATE PROCEDURE sp_calc_stats(IN dept_id INT, OUT emp_count INT, OUT avg_salary DECIMAL(10,2))
BEGIN
    SELECT COUNT(*), AVG(salary) INTO emp_count, avg_salary 
    FROM employees WHERE department_id = dept_id;
END

// Java 調用
String sql = "{CALL sp_calc_stats(?, ?, ?)}";
try (CallableStatement cstmt = conn.prepareCall(sql)) {
    cstmt.setInt(1, deptId);
    cstmt.registerOutParameter(2, Types.INTEGER);
    cstmt.registerOutParameter(3, Types.DECIMAL);
    
    cstmt.execute();
    
    int count = cstmt.getInt(2);
    BigDecimal avg = cstmt.getBigDecimal(3);
    System.out.println("員工數: " + count + ", 平均薪資: " + avg);
}

Spring Data JPA / MyBatis 整合

Spring Data JPA

@Repository
public interface EmployeeRepository extends JpaRepository<Employee, Integer> {
    
    @Procedure(name = "sp_emps_in_dept")
    List<EmployeeProjection> callEmpsInDept(@Param("dept_id") Integer deptId);
    
    // 輸出參數
    @Procedure
    Map<String, Object> calcStats(@Param("dept_id") Integer deptId);
}

MyBatis

<select id="callEmpsInDept" statementType="CALLABLE" resultMap="empResultMap">
    {CALL sp_emps_in_dept(#{deptId, mode=IN, jdbcType=INTEGER})}
</select>

<select id="calcStats" statementType="CALLABLE">
    {CALL sp_calc_stats(
        #{deptId, mode=IN, jdbcType=INTEGER},
        #{empCount, mode=OUT, jdbcType=INTEGER},
        #{avgSalary, mode=OUT, jdbcType=DECIMAL}
    )}
</select>

最佳實踐與避坑指南

  1. 連接池配置:預存過程可能長時間運行,確保連接池大小足夠,設置 maxLifetime 避免連接失效
  2. 事務邊界:預存過程內部事務由數據庫管理,Java 端通常不需要額外事務控制
  3. 結果集關閉:必須顯式關閉每個 ResultSet,否則游標洩漏導致連接不可用
  4. 異常處理:捕獲 SQLException 解析錯誤碼(MySQL 錯誤碼 13xx 為預存過程相關)
  5. 版本控制:預存過程腳本納入版本控制,避免數據庫與代碼不同步
  6. 性能監控:慢查詢日誌監控預存過程執行時間,避免複雜邏輯阻塞數據庫
  7. 避免過度使用:簡單 CRUD 用 ORM,複雜報表、批量計算、跨表事務才用預存過程

調試技巧

  • MySQL Workbench 直接調用測試:CALL sp_emps_in_dept(10);
  • 開啟 JDBC 驅動調試日誌:log4j.logger.java.sql=DEBUG
  • 使用 SHOW PROCESSLIST 觀察長時間運行的預存過程
  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved