對比 Statement,PreparedStatement 提供三大核心優勢:
?)與數據分離,驅動程序負責轉義,從根本上杜絕拼接 SQL 帶來的注入風險。setXxx(index, value))清晰分離 SQL 模板與業務數據,避免字串拼接錯誤。適用場景:同一 SQL 結構、不同參數值的重複執行(批量插入、條件查詢、分頁等)。單次執行的臨時 SQL 用 Statement 即可,無額外收益。
// 1. 獲取預編譯語句對象
String sql = "SELECT * FROM student WHERE YEAR(birthday) BETWEEN ? AND ?";
PreparedStatement ps = conn.prepareStatement(sql);
// 2. 設置參數(索引從 1 開始)
ps.setString(1, "1987");
ps.setString(2, "1990");
// 3. 執行查詢
ResultSet rs = ps.executeQuery();
// 4. 執行增刪改
int affected = ps.executeUpdate();
public ResultSet queryByRange(String sql, String param1, String param2) throws SQLException {
getConnection();
PreparedStatement ps = conn.prepareStatement(sql);
ps.setString(1, param1);
ps.setString(2, param2);
return ps.executeQuery();
}
// 調用
ResultSet rs = dao.queryByRange(
"SELECT * FROM t_user WHERE YEAR(birthdate) BETWEEN ? AND ?",
"1992", "1992"
);
/**
* 通用預編譯查詢
* @param sql 含佔位符的 SQL
* @param params 參數值數組,順序對應佔位符
* @return ResultSet(調用者負責關閉)
*/
public ResultSet query(String sql, Object... params) throws SQLException {
getConnection();
PreparedStatement ps = conn.prepareStatement(sql);
for (int i = 0; i < params.length; i++) {
ps.setObject(i + 1, params[i]); // setObject 自動處理類型轉換
}
return ps.executeQuery();
}
// 調用示例
// 生日範圍查詢
ResultSet rs1 = dao.query(
"SELECT * FROM t_user WHERE YEAR(birthdate) BETWEEN ? AND ?",
"1992", "1992"
);
// ID 範圍查詢
ResultSet rs2 = dao.query(
"SELECT * FROM t_user WHERE id BETWEEN ? AND ?",
100, 200
);
/**
* 通用預編譯增刪改
* @param sql 含佔位符的 SQL
* @param params 參數值數組
* @return 影響行數
*/
public int executeUpdate(String sql, Object... params) throws SQLException {
int count = 0;
getConnection();
try {
PreparedStatement ps = conn.prepareStatement(sql);
if (params != null) {
for (int i = 0; i < params.length; i++) {
ps.setObject(i + 1, params[i]);
}
}
count = ps.executeUpdate();
} finally {
close(); // 關閉連接、語句、結果集
}
return count;
}
// 批量插入 10 條測試數據
for (int i = 0; i < 10; i++) {
dao.executeUpdate(
"INSERT INTO jdbc_test(username, password) VALUES(?, ?)",
"user" + i, "pwd" + i
);
}
// 批量刪除
dao.executeUpdate(
"DELETE FROM t_user WHERE id BETWEEN ? AND ?",
1010, 1030
);
大量寫入時,使用 addBatch() + executeBatch() 減少網絡往返:
public int[] batchInsert(String sql, List
雖然 setObject 通用,但明確類型方法(setInt、setString、setTimestamp)能更早發現類型不匹配,建議在核心業務 DAO 中使用強類型設置。
// 使用 try-with-resources (JDK 7+) 自動關閉資源
public ResultSet queryAutoClose(String sql, Object... params) throws SQLException {
try (Connection conn = dataSource.getConnection();
PreparedStatement ps = conn.prepareStatement(sql)) {
for (int i = 0; i < params.length; i++) {
ps.setObject(i + 1, params[i]);
}
// 注意:ResultSet 需在 Connection 關閉前讀完,或獨立管理
return ps.executeQuery();
}
}
預編譯階段數據庫已生成執行計劃,參數作為純數據傳輸,不會再經過 SQL 解析器,因此即使參數包含 ' OR '1'='1 也僅作為字面值查詢,無法改變 SQL 語義。
| 錯誤現象 | 原因 | 修正 |
|---|---|---|
| 佔位符索引報錯 | 索引從 1 開始,而非 0 | 循環用 i + 1 |
| 參數數量不匹配 | SQL 中 ? 數量與 params 長度不一致 | 開發期單元測試驗證 |
| 日期類型插入失敗 | 直接 setObject 可能類型映射失敗 | 用 setTimestamp 或 setDate |
| 批量執行部分失敗無感知 | executeBatch 返回數組需逐項檢查 | 檢查返回值,或捕獲 BatchUpdateException |
| ResultSet 使用中連接被關閉 | DAO 方法關閉連接,調用者還在遍歷 | 調用者負責關閉,或在 Service 層統一事務邊界 |
PreparedStatement 是 JDBC 開發的基石,掌握「預編譯機制、參數化設置、批量優化、資源管理」四大核心,並封裝為通用 DAO 基類,能顯著提升開發效率與代碼質量。實戰中建議:查詢用動態參數數組封裝,寫入用批量事務,資源用 try-with-resources,類型用強類型設置,構建安全、高效、可維護的持久層。