無論你用 MyBatis、Hibernate、JPA 還是 Spring Data,底層皆是 JDBC。掌握原生 API 能讓你排查疑難雜癥、優化性能、理解框架原理。本文系統梳理 標準流程、四大核心對象、進階特性(事務/批量/可滾動結果集),附最佳實踐代碼模板。
ConnectionStatement / PreparedStatementResultSet (查詢) / int 影響行數 (更新)ResultSet → Statement → ConnectionString url = "jdbc:mysql://localhost:3306/mydb?useSSL=false&serverTimezone=UTC";
String user = "root", pwd = "123456";
Class.forName("com.mysql.cj.jdbc.Driver"); // JDBC 4.0+ 可省略
Connection conn = DriverManager.getConnection(url, user, pwd);
// web.xml / context.xml 配置資源引用
Context ctx = new InitialContext();
DataSource ds = (DataSource) ctx.lookup("java:comp/env/jdbc/myds");
Connection conn = ds.getConnection(); // 從連接池獲取,用完 close() 歸還池中
優勢:連接池管理、事務集成、安全隔離、配置與代碼分離。
| 特性 | Statement | PreparedStatement |
|---|---|---|
| SQL 注入防護 | 無,需手工轉義 | 參數化佔位符 ?,驅動層自動處理 |
| 性能(重複執行) | 每次編譯 | 預編譯一次,多次綁定參數執行 |
| 批量更新 | addBatch(sql) | setXxx() → addBatch() → executeBatch() |
| Blob/Clob 處理 | 困難 | 原生支持 setBlob/setClob |
原則:生產代碼 99% 用 PreparedStatement。僅在 DDL、一次性腳本用 Statement。
// PreparedStatement 查詢範例
String sql = "SELECT id, name, age FROM users WHERE age > ? AND status = ?";
try (PreparedStatement ps = conn.prepareStatement(sql)) {
ps.setInt(1, 18);
ps.setString(2, "active");
try (ResultSet rs = ps.executeQuery()) {
while (rs.next()) {
System.out.println(rs.getInt("id") + " " + rs.getString("name"));
}
}
}
// 更新範例
String upd = "UPDATE users SET age = ? WHERE id = ?";
try (PreparedStatement ps = conn.prepareStatement(upd)) {
ps.setInt(1, 20);
ps.setLong(2, 1001L);
int rows = ps.executeUpdate();
System.out.println("影響行數: " + rows);
}
// 創建可滾動、只讀結果集
Statement stmt = conn.createStatement(
ResultSet.TYPE_SCROLL_INSENSITIVE, // 不感知底層數據變化
ResultSet.CONCUR_READ_ONLY
);
ResultSet rs = stmt.executeQuery("SELECT * FROM large_table");
rs.absolute(9000); // 直接跳轉到第 9000 行
rs.beforeFirst(); // 回到起始位置
rs.afterLast(); // 移到末尾
類型選擇:TYPE_SCROLL_INSENSITIVE 性能較好;TYPE_SCROLL_SENSITIVE 能看到別的事務提交的修改,開銷大。
// 模式 A: Statement 批量執行不同 SQL
Statement stmt = conn.createStatement();
stmt.addBatch("INSERT INTO log VALUES (1, 'msg1')");
stmt.addBatch("UPDATE config SET val='x' WHERE k='y'");
stmt.addBatch("DELETE FROM temp WHERE id < 100");
int[] counts = stmt.executeBatch(); // 返回每條影響行數
// 模式 B: PreparedStatement 同一 SQL 多組參數(更常用)
String sql = "INSERT INTO orders (user_id, amount) VALUES (?, ?)";
try (PreparedStatement ps = conn.prepareStatement(sql)) {
for (Order o : orderList) {
ps.setLong(1, o.getUserId());
ps.setBigDecimal(2, o.getAmount());
ps.addBatch();
if (++count % 1000 == 0) { // 分批提交,防止內存溢出
ps.executeBatch();
conn.commit();
}
}
ps.executeBatch(); // 剩餘
conn.commit();
}
conn.setAutoCommit(false); // 關鍵:關閉自動提交
try {
// 業務邏輯:多條 SQL 組成原子操作
transfer(fromAccount, toAccount, amount); // 內部用同一 conn 執行多條 update
updateLedger(fromAccount, -amount);
updateLedger(toAccount, amount);
conn.commit(); // 全部成功提交
} catch (Exception e) {
conn.rollback(); // 任一失敗回滾
throw e;
} finally {
conn.setAutoCommit(true); // 恢復默認行為
}
關鍵規則:同一事務內,每條 SQL 執行前必須關閉上一條的 Statement/PreparedStatement(或使用 try-with-resources 自動關閉),否則可能導致鎖競爭、游標泄漏。
public void queryUsers(int minAge) {
String sql = "SELECT id, name FROM users WHERE age > ?";
// 自動關閉 conn、ps、rs,即使拋異常
try (Connection conn = ds.getConnection();
PreparedStatement ps = conn.prepareStatement(sql)) {
ps.setInt(1, minAge);
try (ResultSet rs = ps.executeQuery()) {
while (rs.next()) {
// 處理數據
}
}
} catch (SQLException e) {
log.error("查詢失敗", e);
throw new RuntimeException(e);
}
}
? 佔位符。setFetchSize(Integer.MIN_VALUE) 啟用流式讀取。@Transactional (Spring) 或模板方法隔離。executeBatch() + commit()。JDBC 雖「原始」,卻是所有上層框架的基石。熟練掌握 連接獲取、PreparedStatement 參數化、ResultSet 流式讀取、事務邊界控制、批量寫入優化,能讓你在性能調優、疑難排查、框架選型時游刃有餘。下一步建議深入:連接池原理 (HikariCP)、數據庫鎖機制、分布式事務 (Seata/AT 模式)。