PreparedStatement 防注入,支援實體類反射映射與 @Column 註解自定義欄位名。public class DBUtil {
// 單例 DataSource / 連線池(此處示範簡單 DriverManager)
private static final String URL = "jdbc:postgresql://192.168.56.101/db";
private static final String USER = "test";
private static final String PASS = "12345678";
private static String driverClass; // 連線後動態偵測
static {
try { Class.forName("org.postgresql.Driver"); }
catch (Exception e) { throw new ExceptionInInitializerError(e); }
}
// 內部:取得連線並偵測驅動類別
private static Connection getConnection() throws SQLException {
Connection conn = DriverManager.getConnection(URL, USER, PASS);
driverClass = conn.getMetaData().getDriverName(); // 實際為驅動類名
return conn;
}
// 內部:統一關閉資源
private static void close(Connection c, Statement s, ResultSet r) {
closeQuietly(r); closeQuietly(s); closeQuietly(c);
}
private static void closeQuietly(AutoCloseable c) {
if (c != null) try { c.close(); } catch (Exception ignored) {}
}
}
根據偵測到的驅動類別,自動生成對應方言的 LIMIT/OFFSET、ROWNUM、TOP 等分頁語法。
private static String buildPageSql(String sql, int offset, int limit) {
StringBuilder sb = new StringBuilder();
if (driverClass.contains("MySQL") || driverClass.contains("PostgreSQL")) {
sb.append(sql).append(" LIMIT ").append(limit).append(" OFFSET ").append(offset);
} else if (driverClass.contains("Oracle")) {
// Oracle 12c+ 可用 OFFSET/FETCH,此處示範傳統 ROWNUM 寫法
List<String> cols = extractSelectColumns(sql);
sb.append("SELECT ").append(String.join(", ", cols))
.append(" FROM (").append(sql).append(") WHERE ROWNUM BETWEEN ")
.append(offset + 1).append(" AND ").append(offset + limit);
} else if (driverClass.contains("SQLServer")) {
// SQL Server 2012+ OFFSET/FETCH
sb.append(sql).append(" ORDER BY (SELECT 0) OFFSET ").append(offset).append(" ROWS FETCH NEXT ").append(limit).append(" ROWS ONLY");
} else {
throw new UnsupportedOperationException("Unsupported dialect: " + driverClass);
}
return sb.toString();
}
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Column {
String name() default "";
}
// 使用範例
public class User {
@Column(name = "user_id") private Long id;
@Column(name = "user_name") private String name;
private String email; // 欄位名與屬性名相同,無需註解
// getter/setter 略
}
// 插入:自動解析 INSERT SQL 欄位順序,反射取值綁定參數
public static <T> int insert(String sql, T entity) { ... }
// 更新:解析 UPDATE SET 及 WHERE 條件欄位
public static <T> int update(String sql, T entity) { ... }
// 刪除/批量更新:參數陣列版
public static int update(String sql, Object... params) { ... }
// 查詢多筆
public static <T> List<T> queryList(Class<T> clazz, String sql, Object... params) { ... }
// 分頁查詢
public static <T> List<T> queryPage(Class<T> clazz, int offset, int limit, String sql, Object... params) {
sql = buildPageSql(sql, offset, limit);
return queryList(clazz, sql, params);
}
// 查詢單筆
public static <T> T queryOne(Class<T> clazz, String sql, Object... params) { ... }
// 統計總筆數(自動包裝 COUNT(*))
public static int count(String sql, Object... params) { ... }
private static void bindParams(PreparedStatement ps, Object... params) throws SQLException {
if (params == null) return;
for (int i = 0; i < params.length; i++) {
Object v = params[i];
int idx = i + 1;
if (v == null) ps.setNull(idx, Types.VARCHAR);
else if (v instanceof String) ps.setString(idx, (String) v);
else if (v instanceof Integer) ps.setInt(idx, (Integer) v);
else if (v instanceof Long) ps.setLong(idx, (Long) v);
else if (v instanceof Double) ps.setDouble(idx, (Double) v);
else if (v instanceof Boolean) ps.setBoolean(idx, (Boolean) v);
else if (v instanceof Date) ps.setDate(idx, (Date) v);
else if (v instanceof Timestamp) ps.setTimestamp(idx, (Timestamp) v);
else ps.setObject(idx, v);
}
}
// 插入
User u = new User(); u.setName("Alice"); u.setEmail("alice@example.com");
DBUtil.insert("INSERT INTO users (user_name, email) VALUES (?, ?)", u);
// 分頁查詢
List<User> page = DBUtil.queryPage(User.class, 0, 10,
"SELECT user_id, user_name, email FROM users WHERE email LIKE ?", "%@example.com");
// 統計
int total = DBUtil.count("SELECT * FROM users WHERE email LIKE ?", "%@example.com");
| 優點 | 缺點/待改進 |
|---|---|
| 零依賴、輕量、支援多資料庫方言 | SQL 仍需手寫,無類型安全查詢 DSL |
| 反射映射支援 @Column 自定義欄位 | 效能較 ORM 稍低(可快取 Field/Column 資訊) |
| 自動計數與分頁改寫 | 複雜 JOIN/子查詢的欄位提取可能失敗 |
| 參數化防注入 | 無連線池整合(需自行注入 DataSource) |