早期學 Servlet 時手寫的簡易 JDBC 包裝,畢業後重構改進。雖技術棧已轉向 Spring Data JPA / MyBatis,但理解底層 Connection/PreparedStatement/ResultSet 生命週期管理,對排查連線洩漏、SQL 注入、事務邊界仍有核心價值。
package dao;
import java.sql.*;
public class BaseDao {
// 建議改為從配置檔/環境變數讀取
private static final String URL = "jdbc:sqlserver://localhost:1433;DatabaseName=StudentManageSys";
private static final String USER = "sa";
private static final String PWD = "sa";
protected Connection conn = null;
protected PreparedStatement pstmt = null;
static {
try {
Class.forName("com.microsoft.sqlserver.jdbc.SQLServerDriver");
} catch (ClassNotFoundException e) {
throw new ExceptionInInitializerError("JDBC Driver 載入失敗", e);
}
}
/** 取得連線(支援外部傳入連線以實作事務) */
protected Connection getConnection() throws SQLException {
if (conn == null || conn.isClosed()) {
conn = DriverManager.getConnection(URL, USER, PWD);
}
return conn;
}
/** 統一關閉資源:順序 ResultSet → Statement → Connection */
protected void closeAll(ResultSet rs, Statement stmt, Connection conn) {
try { if (rs != null) rs.close(); } catch (SQLException ignore) {}
try { if (stmt != null) stmt.close(); } catch (SQLException ignore) {}
try { if (conn != null) conn.close(); } catch (SQLException ignore) {}
}
/** 通用執行入口:回傳 ResultSet 或 update count */
protected Object execute(String sql, Object... params) throws SQLException {
prepareStatement(sql, params);
boolean hasResultSet = pstmt.execute();
return hasResultSet ? pstmt.getResultSet() : pstmt.getUpdateCount();
}
private void prepareStatement(String sql, Object[] params) throws SQLException {
conn = getConnection();
pstmt = conn.prepareStatement(sql);
if (params != null) {
for (int i = 0; i < params.length; i++) {
pstmt.setObject(i + 1, params[i]); // 防 SQL 注入
}
}
}
}
| 原版問題 | 改進方案 |
|---|---|
| 靜態區塊吞噬異常,導致後續 NPE | 拋出 ExceptionInInitializerError 快速失敗 |
closeAll 參數順序錯誤,連線過早關閉導致 RS/Stmt 無法關閉 | 順序:RS → Stmt → Conn,各自獨立 try-catch |
| 無事務支援(conn 為實例變數,無法外部控制) | 保留 getConnection() 可覆寫,子類可注入共用連線 |
| 硬編碼資料庫連線資訊 | 建議整合 DataSource (DBCP/HikariCP) + 外部配置 |
| 未使用 try-with-resources (Java 7+) | 簡化版保留手動關閉以教學;生產建議全面遷移至 JdbcTemplate/MyBatis |
public class StudentDao extends BaseDao {
public List findAll() {
String sql = "SELECT id, name, age FROM student";
List list = new ArrayList<>();
ResultSet rs = null;
try {
rs = (ResultSet) execute(sql);
while (rs.next()) {
Student s = new Student();
s.setId(rs.getInt("id"));
s.setName(rs.getString("name"));
s.setAge(rs.getInt("age"));
list.add(s);
}
} catch (SQLException e) {
throw new RuntimeException("查詢學生失敗", e);
} finally {
closeAll(rs, pstmt, conn);
}
return list;
}
}
BaseDao 承載了 JDBC 原生 API 的精華與痛點。理解它,是通往成熟 ORM 框架的階梯;超越它,才是現代 JavaWeb 開發的常態。祝各位天天進步,早日脫離「手寫 Dao」苦海!