前言
JDBC (Java Database Connectivity) 是 Java 操作關聯式資料庫的標準 API。雖然現代開發多使用 ORM 框架 (MyBatis, JPA/Hibernate),但理解 JDBC 核心流程、驅動機制、設計模式對於除錯、效能調優、框架原理掌握至關重要。
一、JDBC 七大標準步驟
- 載入驅動:
Class.forName("com.mysql.cj.jdbc.Driver")
- 建立連線:
DriverManager.getConnection(url, user, pwd)
- 建立 Statement:
conn.createStatement() / prepareStatement(sql) / prepareCall(sql)
- 執行 SQL:
executeQuery() (查詢) / executeUpdate() (增刪改) / execute() (通用)
- 處理結果:
ResultSet 遍歷 / 影響行數
- 釋放資源:關閉
ResultSet → Statement → Connection (逆序)
- 異常處理:捕獲
SQLException、ClassNotFoundException
二、完整範例代碼 (MySQL 8.0+)
import java.sql.*;
public class JdbcDemo {
// 資料庫配置 (實務應外部化至配置檔/環境變數)
private static final String URL = "jdbc:mysql://localhost:3306/mydb?useSSL=false&serverTimezone=UTC&characterEncoding=utf-8";
private static final String USER = "root";
private static final String PASSWORD = "secret";
private static final String DRIVER = "com.mysql.cj.jdbc.Driver"; // MySQL 8.0+ 驅動類
public static void main(String[] args) {
Connection conn = null;
PreparedStatement ps = null;
ResultSet rs = null;
try {
// 1. 載入驅動 (JDBC 4.0+ SPI 機制可省略,但建議保留確保兼容)
Class.forName(DRIVER);
System.out.println("✅ 驅動載入成功");
// 2. 建立連線
conn = DriverManager.getConnection(URL, USER, PASSWORD);
System.out.println("✅ 連線成功: " + conn.getMetaData().getURL());
// 3. 建立 PreparedStatement (防 SQL 注入、預編譯優化)
String sql = "SELECT id, username, email, create_time FROM users WHERE status = ? AND age > ?";
ps = conn.prepareStatement(sql);
// 4. 設定參數
ps.setInt(1, 1); // status = 1 (啟用)
ps.setInt(2, 18); // age > 18
// 5. 執行查詢
rs = ps.executeQuery();
// 6. 處理結果集
System.out.println("ID\tUsername\tEmail\t\t\tCreate Time");
while (rs.next()) {
// 取得欄位值 (支援索引或名稱,索引效能略高)
int id = rs.getInt("id");
String username = rs.getString("username");
String email = rs.getString("email");
Timestamp createTime = rs.getTimestamp("create_time");
System.out.printf("%d\t%s\t%s\t%s%n", id, username, email, createTime);
}
} catch (ClassNotFoundException e) {
System.err.println("❌ 找不到驅動類別: " + DRIVER);
e.printStackTrace();
} catch (SQLException e) {
System.err.println("❌ 資料庫操作失敗: " + e.getMessage());
e.printStackTrace();
} finally {
// 7. 釋放資源 (逆序關閉,各自 try-catch 避免前一個失敗影響後續)
closeQuietly(rs);
closeQuietly(ps);
closeQuietly(conn);
}
}
// 安全關閉工具方法
private static void closeQuietly(AutoCloseable resource) {
if (resource != null) {
try { resource.close(); } catch (Exception ignored) {}
}
}
}
三、驅動載入機制深度解析
1. Class.forName() 發生了什麼?
// JDBC 驅動類典型結構 (以 MySQL 為例)
package com.mysql.cj.jdbc;
public class Driver extends NonRegisteringDriver implements java.sql.Driver {
// 靜態初始化塊:向 DriverManager 註冊自己
static {
try {
java.sql.DriverManager.registerDriver(new Driver());
} catch (SQLException e) {
throw new RuntimeException("Can't register driver!", e);
}
}
// 建構子通常為空
public Driver() throws SQLException { }
}
Class.forName("com.mysql.cj.jdbc.Driver") 觸發類載入 → 執行靜態初始化塊 → 向 DriverManager 註冊驅動實例。
- JDBC 4.0+ (Java 6+) SPI 機制:驅動 JAR
META-INF/services/java.sql.Driver 文件列出驅動類名,**DriverManager 會自動發現並載入**,無需顯式 Class.forName()。
- **建議保留顯式載入**:確保舊環境兼容、明確依賴、便於診斷驅動缺失問題。
2. DriverManager 獲取連線流程
// 簡化版 DriverManager.getConnection 邏輯
public static Connection getConnection(String url, String user, String pwd) throws SQLException {
// 1. 遍歷已註冊驅動列表
for (DriverInfo di : registeredDrivers) {
Driver driver = di.driver;
// 2. 嘗試連線 (驅動內部解析 URL 判斷是否支援)
if (driver.acceptsURL(url)) {
// 3. 呼叫驅動 connect 方法
Connection conn = driver.connect(url, props); // props 包含 user, password
if (conn != null) return conn;
}
}
throw new SQLException("No suitable driver found for " + url);
}
四、三大 Statement 類型對比
| 類型 | 建立方式 | 適用場景 | SQL 注入防護 | 效能 |
| Statement | conn.createStatement() | 靜態 SQL、無參數、DDL (建表/索引) | ❌ 無 (需自行拼接) | 每次執行都編譯 |
| PreparedStatement | conn.prepareStatement(sql) | **95%+ 場景**:含參數查詢、批次處理、重複執行 | ✅ 參數化預編譯 | 預編譯一次、多次執行、DB 快取計劃 |
| CallableStatement | conn.prepareCall("{call proc(?,?)}") | 呼叫預存程序/函數 | ✅ 參數化 | 依賴 DB 端編譯 |
PreparedStatement 批次處理範例
String sql = "INSERT INTO logs (level, message, create_time) VALUES (?, ?, ?)";
try (PreparedStatement ps = conn.prepareStatement(sql)) {
for (LogEntry log : logs) {
ps.setString(1, log.getLevel());
ps.setString(2, log.getMessage());
ps.setTimestamp(3, Timestamp.valueOf(log.getTime()));
ps.addBatch(); // 加入批次
}
int[] results = ps.executeBatch(); // 一次性執行
// results[i] 為各條影響行數 (通常為 1)
}
五、ResultSet 進階操作
| 特性 | 設定方式 | 說明 |
| 游標類型 | createStatement(ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_READ_ONLY) | TYPE_FORWARD_ONLY (預設, 僅向前) / TYPE_SCROLL_INSENSITIVE (可滾動, 不見他人修改) / TYPE_SCROLL_SENSITIVE (可滾動, 見他人修改) |
| 並發性 | CONCUR_READ_ONLY (預設) / CONCUR_UPDATABLE (可更新) | 可更新 ResultSet 可直接 rs.updateXxx(), rs.insertRow(), rs.deleteRow() |
| 獲取大物件 | rs.getBlob(), rs.getClob(), rs.getBinaryStream(), rs.getCharacterStream() | 避免一次性載入記憶體,流式處理 |
| 檢索大小 | ps.setFetchSize(100) | 控制每次從 DB 拉取行數,平衡記憶體/網路往返 |
六、設計模式在 JDBC 中的應用
1. 工廠模式:DataSource / ConnectionPool
// 標準 DataSource 介面 (javax.sql.DataSource)
// 實作:HikariCP, Druid, DBCP2, Tomcat JDBC Pool
DataSource ds = new HikariDataSource(); // 配置屬性
Connection conn = ds.getConnection(); // 從池取得,用完 close() 歸還池
- **隱藏建立連線細節** (驅動、URL、認證、池化參數)
- **統一資源管理**:取得/歸還、洩漏偵測、閒置回收、效能監控
2. 模板方法模式:JdbcTemplate (Spring)
// Spring JdbcTemplate 核心邏輯簡化
public T query(String sql, RowMapper mapper, Object... args) {
Connection conn = null;
PreparedStatement ps = null;
ResultSet rs = null;
try {
conn = dataSource.getConnection();
ps = conn.prepareStatement(sql);
// 設定參數...
rs = ps.executeQuery();
List results = new ArrayList<>();
int rowNum = 0;
while (rs.next()) {
results.add(mapper.mapRow(rs, rowNum++));
}
return results;
} catch (SQLException e) {
throw translateException(e); // 統一異常轉換
} finally {
// 關閉資源...
}
}
- **固定流程骨架** (連線、語句、參數、執行、結果映射、資源釋放、異常轉換)
- **變化點抽象** : SQL、參數綁定、RowMapper
3. 建造者模式:查詢構建器 (QueryDSL, jOOQ, MyBatis 動態 SQL)
// QueryDSL 流式 API
List users = queryFactory.selectFrom(user)
.where(user.status.eq(1).and(user.age.gt(18)))
.orderBy(user.createTime.desc())
.limit(10)
.fetch();
七、資源管理鐵律:try-with-resources (Java 7+)
// ✅ 標準寫法:自動關閉、異常安全、程式碼簡潔
try (Connection conn = ds.getConnection();
PreparedStatement ps = conn.prepareStatement(sql);
ResultSet rs = ps.executeQuery()) {
// 業務邏輯
while (rs.next()) { ... }
} // 編譯器自動生成 finally 關閉 rs, ps, conn (逆序)
// ❌ 手動 finally 易漏關、巢狀深、異常掩蓋
Connection conn = null;
try {
conn = ds.getConnection();
// ...
} finally {
if (conn != null) try { conn.close(); } catch (SQLException ignored) {}
}
八、常見坑點與最佳實踐
| 坑點 | 後果 | 最佳實踐 |
| 未關閉資源 (Connection/Statement/ResultSet) | 連線洩漏 → 連線池耗盡 → 系統癱瘓 | **強制 try-with-resources** / 連線池洩漏偵測 |
| Statement 拼接 SQL 參數 | SQL 注入風險、計劃快取失效 | **永遠使用 PreparedStatement** 參數化 |
| 事務邊界不清 (自動提交) | 資料不一致、死鎖 | 明確 conn.setAutoCommit(false)、commit()/rollback() |
| ResultSet 獲取欄位用索引而非名稱 | Schema 變更導致錯誤 | 優先用 rs.getString("column_name") 可讀性高、重構安全 |
| 大結果集一次性載入記憶體 | OOM | 游標分頁、流式處理、setFetchSize |
| 驅動版本與資料庫不匹配 | 功能失效、效能差、安全漏洞 | 鎖定驅動版本、定期更新、對應 DB 版本 |
九、從 JDBC 到現代數據存取架構演進
JDBC (原生)
↓ 封裝資源管理、異常轉換、模板方法
Spring JdbcTemplate / Apache DbUtils
↓ 映射物件關係、動態 SQL、緩存、延遲載入
MyBatis / Hibernate (JPA)
↓ 響應式、非阻塞、背壓
R2DBC (Reactive Relational Database Connectivity)
↓ 編譯期類型安全、零反射、原生支援
jOOQ / QueryDSL / Exposed (Kotlin)
結語
JDBC 是 Java 數據存取的**基石**。掌握其**七步驟流程**、**驅動 SPI 機制**、**三大 Statement 選型**、**ResultSet 進階特性**、**資源管理鐵律**,以及**工廠/模板/建造者**等設計模式在其中的應用,能讓你在面對任何上層框架 (MyBatis, JPA, Spring Data, R2DBC) 時,都能洞察底層運作、精準診斷問題、做出最優架構決策。
- 上一篇文章: Java 書店系統畢業設計:用戶模組 Swing UI 完整實作 (第三部分)
- 下一篇文章: Java equals、== 與 = 運算子差異深度解析:從記憶體模型到最佳實踐
Copyright ©
程式碼之家 All Rights Reserved