每個專案都要寫「載入驅動 → 獲取連線 → PreparedStatement → 綁參 → 執行 → 關閉資源」的樣板碼,不僅枯燥易錯,還難以統一異常處理、事務控制、SQL 日誌。本文封裝一個可配置多種資料庫、支援參數化 SQL、自動資源管理、提供增刪改查模板方法的 BaseDao,作為專案資料存取層的基石。
package dao;
import java.sql.*;
import java.util.*;
public class BaseDao {
/* ========== 資料庫配置 (建議外部化到 properties/yaml) ========== */
// MySQL
private static final String DRIVER = "com.mysql.cj.jdbc.Driver";
private static final String URL = "jdbc:mysql://127.0.0.1:3306/school?useSSL=false&serverTimezone=UTC";
private static final String USER = "root";
private static final String PASS = "123456";
// Oracle / SQL Server 只需切換上述四個常數即可
// private static final String DRIVER = "oracle.jdbc.driver.OracleDriver";
// private static final String URL = "jdbc:oracle:thin:@localhost:1521:XE";
// ...
static {
try { Class.forName(DRIVER); }
catch (ClassNotFoundException e) { throw new ExceptionInInitializerError("JDBC 驅動載入失敗: " + DRIVER); }
}
/* ========== 連線獲取 ========== */
public Connection getConnection() throws SQLException {
return DriverManager.getConnection(URL, USER, PASS);
}
/* ========== 統一資源關閉 (逆序、逐一 try-catch) ========== */
public void closeAll(Connection conn, PreparedStatement ps, ResultSet rs) {
closeQuietly(rs);
closeQuietly(ps);
closeQuietly(conn);
}
private void closeQuietly(AutoCloseable c) {
if (c != null) try { c.close(); } catch (Exception ignored) {}
}
/* ========== 通用增刪改 (DML) ========== */
/**
* @param sql 參數化 SQL,如 "INSERT INTO t(a,b) VALUES(?,?)"
* @param params 參數陣列,順序對應 ?, 自動 setObject
* @return 影響行數
*/
public int executeUpdate(String sql, Object... params) {
Connection conn = null;
PreparedStatement ps = null;
try {
conn = getConnection();
ps = conn.prepareStatement(sql);
bindParams(ps, params);
return ps.executeUpdate();
} catch (SQLException e) {
throw new RuntimeException("SQL 執行失敗: " + sql, e);
} finally {
closeAll(conn, ps, null);
}
}
/* ========== 通用單筆查詢 ========== */
public T queryOne(String sql, RowMapper mapper, Object... params) {
Connection conn = null;
PreparedStatement ps = null;
ResultSet rs = null;
try {
conn = getConnection();
ps = conn.prepareStatement(sql);
bindParams(ps, params);
rs = ps.executeQuery();
return rs.next() ? mapper.mapRow(rs) : null;
} catch (SQLException e) {
throw new RuntimeException("查詢失敗: " + sql, e);
} finally {
closeAll(conn, ps, rs);
}
}
/* ========== 通用多筆查詢 ========== */
public List queryList(String sql, RowMapper mapper, Object... params) {
Connection conn = null;
PreparedStatement ps = null;
ResultSet rs = null;
List list = new ArrayList<>();
try {
conn = getConnection();
ps = conn.prepareStatement(sql);
bindParams(ps, params);
rs = ps.executeQuery();
while (rs.next()) list.add(mapper.mapRow(rs));
return list;
} catch (SQLException e) {
throw new RuntimeException("查詢失敗: " + sql, e);
} finally {
closeAll(conn, ps, rs);
}
}
/* ========== 參數綁定輔助 ========== */
private void bindParams(PreparedStatement ps, Object[] params) throws SQLException {
if (params != null) {
for (int i = 0; i < params.length; i++) {
ps.setObject(i + 1, params[i]); // JDBC 4.2+ 支援自動類型推導
}
}
}
/* ========== RowMapper 函數式介面 (Java 8+) ========== */
@FunctionalInterface
public interface RowMapper {
T mapRow(ResultSet rs) throws SQLException;
}
/* ========== 測試主程式 ========== */
public static void main(String[] args) {
BaseDao dao = new BaseDao();
try (Connection c = dao.getConnection()) {
System.out.println("連線成功: " + !c.isClosed());
} catch (SQLException e) {
e.printStackTrace();
}
}
}
public class UserDao extends BaseDao {
public User findById(int id) {
return queryOne("SELECT * FROM user WHERE id=?", rs -> {
User u = new User();
u.setId(rs.getInt("id"));
u.setName(rs.getString("name"));
u.setAge(rs.getInt("age"));
return u;
}, id);
}
public List findAll() {
return queryList("SELECT * FROM user", rs -> {
User u = new User();
u.setId(rs.getInt("id"));
u.setName(rs.getString("name"));
u.setAge(rs.getInt("age"));
return u;
});
}
public boolean insert(User u) {
return executeUpdate("INSERT INTO user(name,age) VALUES(?,?)", u.getName(), u.getAge()) == 1;
}
public boolean update(User u) {
return executeUpdate("UPDATE user SET name=?, age=? WHERE id=?", u.getName(), u.getAge(), u.getId()) == 1;
}
public boolean delete(int id) {
return executeUpdate("DELETE FROM user WHERE id=?", id) == 1;
}
}
| 特性 | 實現方式 | 收益 |
|---|---|---|
| 多資料庫切換 | 四個常數 (DRIVER/URL/USER/PASS) | 零程式碼修改遷移 MySQL/Oracle/SQL Server |
| SQL 注入防護 | 全程 PreparedStatement + setObject | 參數化查詢,徹底杜絕拼接注入 |
| 資源零洩漏 | finally + closeQuietly 逆序關閉 | 異常/正常路徑均安全釋放 |
| 函數式映射 | RowMapper Lambda | 映射邏輯內聯,無需匿名類/繼承 |
| 異常豐富化 | RuntimeException 包裝 SQL + 原始異常 | 上層無需捕獲 SQLException,日誌含完整 SQL |
| 可變參數 | Object... params | 呼叫端極簡潔,支援任意參數數量 |
getConnection() 改為從 HikariCP/DataSource 獲取,移除 DriverManager。executeInTransaction(ConnectionCallback cb) ,控制 conn.setAutoCommit(false)、commit/rollback。PreparedStatement 代理,記錄慢 SQL、綁定參數。queryPage(sql, mapper, pageNum, pageSize, params),內部拼接 dialect 特定 LIMIT/OFFSET。@Value("${spring.datasource.xxx}") 或獨立 db.properties。BaseDao 不是框架,而是**團隊共識的起點**。它用最純粹的 JDBC API 解決了 80% 的重複勞動,同時保留了完全的 SQL 掌控權。理解其設計權衡(如為什麼不用 ORM、為什麼拋 RuntimeException),能讓你在面對 MyBatis、JPA、JdbcTemplate 等高階工具時,知道它們在幫你做什麼、哪裡仍需自己把關。