JDBC (Java Database Connectivity) 是 Java 應用與關係型數據庫交互的基石。掌握 Statement、PreparedStatement、CallableStatement 三大核心執行對象的區別與適用場景,是編寫高性能、安全數據訪問代碼的關鍵。本文將從類結構、核心差異、防注入原理、存儲過程調用四個維度深度剖析。
JDBC API 主要分為 java.sql (核心 API) 與 javax.sql (擴展 API,含 DataSource、連接池) 兩個包。核心介面體系如下:
connect(url, props)getConnection(url, user, pwd)createStatement() → Statement (靜態 SQL)prepareStatement(sql) → PreparedStatement (預編譯 SQL)prepareCall(sql) → CallableStatement (存儲過程)next() 移動行,getXxx() 取列值| 維度 | Statement | PreparedStatement |
|---|---|---|
| SQL 執行方式 | 靜態 SQL,每次攜帶完整 SQL 字符串 executeUpdate(sql) |
預編譯 SQL,創建時發送 SQL 模板,執行時僅傳參數 executeUpdate() |
| 性能 | 每次執行都需解析、編譯、優化,重複執行開銷大 | 預編譯一次,多次執行復用執行計劃,批量操作顯著提升吞吐 |
| 安全性 | 字符串拼接極易導致 SQL 注入 | 參數佔位符 ? + 類型安全設置,徹底杜絕注入 |
| 代碼可讀性 | SQL 與參數混雜,難以維護 | SQL 模板與參數分離,結構清晰 |
| 適用場景 | DDL、一次性查詢、無參數動態 SQL | 90%+ 業務場景:增刪改查、批量操作、高併發核心鏈路 |
PreparedStatement 在創建時將 SQL 發送給數據庫服務端進行語法解析、語義分析、優化生成執行計劃,此過程中參數佔位符 ? 已被識別為數據而非代碼。執行階段通過 setXxx(index, value) 傳遞的參數,經過 JDBC 驅動的類型轉換與轉義,直接綁定到已編譯的執行計劃參數槽位,**絕不會再經過 SQL 解析器**,從根本上切斷了注入攻擊路徑。
CallableStatement 繼承自 PreparedStatement,專門用於調用數據庫存儲過程/函數,支援 IN、OUT、INOUT 三種參數模式。
prepareCall("{call proc_name(?, ?)}") 或 {? = call func_name(?, ?)}setXxx(index, val) 綁定 IN/INOUT 參數registerOutParameter(index, SqlType) 註冊 OUT/INOUT 參數類型execute() 執行(無結果集或多結果集時必須用此方法)getXxx(index) 獲取 OUT/INOUT 參數返回值// 存儲過程定義
CREATE PROCEDURE pro_findById @id INT
AS SELECT * FROM tb_user WHERE id = @id;
GO
CREATE PROCEDURE findNameById @id INT, @name VARCHAR(20) OUTPUT
AS SELECT @name = username FROM tb_user WHERE id = @id;
GO
// Java 調用代碼 (JDK 1.7 + SQL Server 2008)
public class StoredProcDemo {
private static final String URL = "jdbc:sqlserver://localhost:1433;DatabaseName=User";
private static final String USER = "sa";
private static final String PWD = "qwer1234";
private static final String DRIVER = "com.microsoft.sqlserver.jdbc.SQLServerDriver";
// 場景 1:無返回值,僅查詢結果集
@Test
public void testQueryProc() throws Exception {
Class.forName(DRIVER);
try (Connection conn = DriverManager.getConnection(URL, USER, PWD);
CallableStatement cs = conn.prepareCall("{call pro_findById(?)}")) {
cs.setInt(1, 3);
try (ResultSet rs = cs.executeQuery()) {
while (rs.next()) {
System.out.printf("%d, %s, %s, %s, %s%n",
rs.getInt("id"), rs.getString("username"),
rs.getString("password"), rs.getString("gender"),
rs.getString("interest"));
}
}
}
}
// 場景 2:有 OUT 參數,需用 execute() 而非 executeQuery()
@Test
public void testOutParamProc() throws Exception {
Class.forName(DRIVER);
try (Connection conn = DriverManager.getConnection(URL, USER, PWD);
CallableStatement cs = conn.prepareCall("{call findNameById(?, ?)}")) {
cs.setInt(1, 3);
cs.registerOutParameter(2, Types.VARCHAR); // 註冊 OUT 參數類型
cs.execute(); // 必須用 execute(),SQL Server 不返回 ResultSet 時 executeQuery() 會報錯
String name = cs.getString(2);
System.out.println("用戶名: " + name);
}
}
}
execute(),不能用 executeQuery(),否則拋出 "No results returned" 錯誤{call pkg.proc(?, ?)},OUT 參數類型用 OracleTypes.CURSOR 等noAccessToProcedureBodies=true 避免元數據查詢開銷PreparedStatement 以預編譯機制同時解決了性能與安全兩大核心痛點,是 JDBC 開發的首選。CallableStatement 則為存儲過程調用提供了標準化介面,掌握 registerOutParameter 與 execute() 的配合使用是關鍵。實際項目中建議封裝通用 DAO 基類或使用 MyBatis/JdbcTemplate 等成熟框架,進一步提升開發效率與代碼質量。
