Last active
August 11, 2025 14:07
-
-
Save kran/48a9e53e89387c57acb97154a8e51eef to your computer and use it in GitHub Desktop.
apache dbutils wrapper
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import org.apache.commons.dbutils.*; | |
import org.apache.commons.dbutils.handlers.*; | |
import javax.sql.DataSource; | |
import java.beans.Introspector; | |
import java.beans.PropertyDescriptor; | |
import java.lang.reflect.Method; | |
import java.math.BigInteger; | |
import java.sql.Connection; | |
import java.sql.SQLException; | |
import java.util.AbstractMap.SimpleEntry; | |
import java.util.*; | |
import java.util.function.Function; | |
import java.util.logging.Logger; | |
import java.util.stream.Collectors; | |
public class Duck { | |
private static final Logger logger = Logger.getLogger(Duck.class.getName()); | |
protected final List<String> queryParts = new ArrayList<>(); | |
protected final List<Object> bindValues = new ArrayList<>(); | |
protected final Map<String, Integer> marks = new HashMap<>(); | |
protected Connection txConn = null; | |
protected Function<String, String> escaper = this.makeEscaper("`"); | |
protected boolean isSnakeCase = true; | |
protected final QueryRunner queryRunner; | |
protected Duck(DataSource dataSource) { | |
queryRunner = new QueryRunner(dataSource); | |
} | |
protected Duck(Connection conn) { | |
queryRunner = new QueryRunner(); | |
txConn = conn; | |
} | |
public static Duck init(DataSource dataSource_) { | |
return new Duck(dataSource_); | |
} | |
public String getSql() { | |
return String.join(" ", queryParts); | |
} | |
public Object[] getParams() { | |
return bindValues.toArray(); | |
} | |
public Duck reset() { | |
this.queryParts.clear(); | |
this.bindValues.clear(); | |
this.marks.clear(); | |
return this; | |
} | |
public Connection getTxConn() { | |
if (txConn == null) { | |
throw new DuckException("Not in a transaction context"); | |
} | |
return txConn; | |
} | |
protected Connection selectConnection() throws SQLException { | |
if (txConn != null) { | |
return txConn; | |
} | |
if (queryRunner.getDataSource() != null) { | |
return queryRunner.getDataSource().getConnection(); | |
} | |
throw new DuckException("Unable to obtain database connection"); | |
} | |
private <T> T executeInConnection(ConnectionCallback<T> action) { | |
Connection conn = null; | |
try { | |
conn = selectConnection(); | |
return action.doInConnection(conn); | |
} catch (SQLException cause) { | |
throw new DuckException(cause); | |
} finally { | |
if (conn != null && conn != txConn) { | |
try { | |
conn.close(); | |
} catch (SQLException ex) { | |
logger.warning("Failed to close connection: " + ex.getMessage()); | |
} | |
} | |
} | |
} | |
public <T> T fetch(final ResultSetHandler<T> rst) { | |
return executeInConnection(conn -> queryRunner.query(conn, getSql(), rst, getParams())); | |
} | |
public <T> List<T> fetchBeanList(Class<T> tClass) { | |
RowProcessor rowProcessor = new BasicRowProcessor(new GenerousBeanProcessor()); | |
ResultSetHandler<List<T>> handler = new BeanListHandler<>(tClass, rowProcessor); | |
return fetch(handler); | |
} | |
public <T> T fetchBean(Class<T> tClass) { | |
List<T> result = fetchBeanList(tClass); | |
if (result.size() > 1) { | |
String errmsg = String.format("Non-unique result: query returned %d rows when expecting exactly one row", result.size()); | |
throw new DuckException(errmsg); | |
} | |
return result.isEmpty() ? null : result.get(0); | |
} | |
public <K, V> Map<K, V> fetchBeanMap(String columnName, Class<V> vClass) { | |
BeanMapHandler<K, V> handler = new BeanMapHandler<>(vClass, columnName); | |
return fetch(handler); | |
} | |
public List<Map<String, Object>> fetchMapList() { | |
MapListHandler handler = new MapListHandler(); | |
return fetch(handler); | |
} | |
public Map<String, Object> fetchMap() { | |
MapHandler handler = new MapHandler(); | |
return fetch(handler); | |
} | |
public <T> T fetchScalar(Class<T> tClass) { | |
ScalarHandler<T> handler = new ScalarHandler<>(); | |
return fetch(handler); | |
} | |
public Duck select(String table, String where, Object... params) { | |
return this.add("select") | |
.mark("F", "*") | |
.add("from " + this.escape(table)) | |
.add("where " + where, params); | |
} | |
public int update() { | |
return executeInConnection(conn -> queryRunner.update(conn, getSql(), getParams())); | |
} | |
public int updateMap(String tableName, Map<String, Object> params, String where, Object... whereParams) { | |
SimpleEntry<String, Object[]> paramsKv = pairUpdate(params); | |
String sql = String.format("update %s set %s", escape(tableName), paramsKv.getKey()); | |
return this.add(sql, paramsKv.getValue()) | |
.add("where " + where, whereParams) | |
.update(); | |
} | |
public int updateBean(String tableName, Object bean, String where, Object... whereParams) { | |
Map<String, Object> params = beanToMap(bean); | |
return updateMap(tableName, params, where, whereParams); | |
} | |
public int delete() { | |
return update(); | |
} | |
public int delete(String tableName, String where, Object... whereParams) { | |
return this | |
.add("delete from " + escape(tableName)) | |
.add("where " + where, whereParams) | |
.delete(); | |
} | |
public <T> T insert(Class<T> tClass) { | |
return executeInConnection(conn -> queryRunner.insert(conn, getSql(), new ScalarHandler<>(), getParams())); | |
} | |
public BigInteger insert() { | |
return insert(BigInteger.class); | |
} | |
public BigInteger insertMap(String tableName, Map<String, Object> params) { | |
return insertMap(tableName, params, BigInteger.class); | |
} | |
public <T> T insertMap(String tableName, Map<String, Object> params, Class<T> tClass) { | |
SimpleEntry<String, List<Object>> kv = pairInsert(params); | |
String sql = String.format("insert into %s (%s) values (?)", escape(tableName), kv.getKey()); | |
return this.add(sql, kv.getValue()).insert(tClass); | |
} | |
public BigInteger insertBean(String tableName, Object bean) { | |
Map<String, Object> params = beanToMap(bean); | |
return this.insertMap(tableName, params); | |
} | |
public <T> List<T> insertBatch(String tableName, List<Map<String, Object>> paramsList, Class<T> tClass) { | |
if (paramsList == null || paramsList.isEmpty()) { | |
throw new DuckException("Batch params empty"); | |
} | |
// 获取第一个Map的所有键并固定顺序 | |
Map<String, Object> firstMap = paramsList.get(0); | |
List<String> keys = new ArrayList<>(firstMap.keySet()); | |
// 构建SQL语句 | |
String fieldStr = keys.stream() | |
.map(it -> escape(isSnakeCase ? toSnakeCase(it) : it)) | |
.collect(Collectors.joining(", ")); | |
String placeholders = String.join(", ", Collections.nCopies(keys.size(), "?")); | |
String sql = String.format("insert into %s (%s) values (%s)", | |
escape(tableName), fieldStr, placeholders); | |
// 构建批量参数数组 | |
Object[][] batchArgs = new Object[paramsList.size()][]; | |
for (int i = 0; i < paramsList.size(); i++) { | |
Map<String, Object> params = paramsList.get(i); | |
Object[] values = new Object[keys.size()]; | |
for (int j = 0; j < keys.size(); j++) { | |
values[j] = params.get(keys.get(j)); | |
} | |
batchArgs[i] = values; | |
} | |
return executeInConnection(conn -> queryRunner.insertBatch(sql, new ColumnListHandler<>(), batchArgs)); | |
} | |
public List<BigInteger> insertBatch(String tableName, List<Map<String, Object>> paramsList) { | |
return insertBatch(tableName, paramsList, BigInteger.class); | |
} | |
public <R> R transaction(Function<Duck, R> action) { | |
if (txConn != null) { | |
throw new DuckException("Nested transactions are not allowed"); | |
} | |
Connection conn = null; | |
try { | |
conn = queryRunner.getDataSource().getConnection(); | |
conn.setAutoCommit(false); | |
Duck tx = new Duck(conn); | |
R result = action.apply(tx); | |
conn.commit(); | |
return result; | |
} catch (Exception e) { | |
try { | |
if (conn != null) { | |
conn.rollback(); | |
} | |
} catch (SQLException ex) { | |
throw new DuckException("Failed to rollback transaction", ex); | |
} | |
throw new DuckException("Transaction failed", e); | |
} finally { | |
try { | |
if (conn != null) { | |
conn.setAutoCommit(true); | |
conn.close(); | |
} | |
} catch (SQLException e) { | |
logger.warning("Failed to close connection: " + e.getMessage()); | |
} | |
} | |
} | |
protected Duck copy() { | |
Duck query = new Duck(queryRunner.getDataSource()); | |
query.txConn = txConn; | |
query.queryParts.addAll(queryParts); | |
query.bindValues.addAll(bindValues); | |
query.marks.putAll(marks); | |
query.escaper = escaper; | |
query.isSnakeCase = isSnakeCase; | |
return query; | |
} | |
public Duck escaper(Function<String, String> escaper_) { | |
Duck query = copy(); | |
query.escaper = escaper_; | |
return query; | |
} | |
public Function<String, String> makeEscaper(String quote) { | |
return identifier -> { | |
if (identifier == null) { | |
throw new DuckException("Identifier cannot be null"); | |
} | |
if (identifier.trim().isEmpty()) { | |
throw new DuckException("Identifier cannot be empty"); | |
} | |
return Arrays.stream(identifier.split("\\.")) | |
.map(part -> quote | |
+ part.replace(quote, quote+quote) | |
+ quote) | |
.collect(Collectors.joining(".")); | |
}; | |
} | |
public String escape(String ident) { | |
return escaper.apply(ident); | |
} | |
public Duck add(String sql, Object... params) { | |
Duck query = copy(); | |
query.appendParams(sql, params); | |
return query; | |
} | |
public Duck snakeCase(boolean yes) { | |
Duck duck = copy(); | |
duck.isSnakeCase = yes; | |
return duck; | |
} | |
public Duck mark(String name, String sql) { | |
Duck query = copy(); | |
if (query.marks.containsKey(name)) { | |
query.queryParts.set(query.marks.get(name), sql); | |
} else { | |
query.queryParts.add(sql); | |
query.marks.put(name, query.queryParts.size() - 1); | |
} | |
return query; | |
} | |
protected void appendParams(String sql, Object[] args) { | |
String[] parts = (sql + " ").split("\\?"); | |
if (parts.length != args.length + 1) { | |
String errmsg = String.format("Placeholders length (%d) doesn't match parameters length (%d)", | |
parts.length - 1, args.length); | |
throw new DuckException(errmsg); | |
} | |
for (int i = 0; i < parts.length; i++) { | |
if (args.length <= i) { | |
queryParts.add(parts[i]); | |
return; | |
} | |
Object arg = args[i]; | |
if (arg == null) { | |
queryParts.add(parts[i] + '?'); | |
bindValues.add(null); | |
} else if (arg instanceof Collection) { | |
appendArray(parts[i], ((Collection<?>) arg).toArray()); | |
} else if (arg.getClass().isArray()) { | |
appendArray(parts[i], (Object[]) arg); | |
} else { | |
queryParts.add(parts[i] + '?'); | |
bindValues.add(arg); | |
} | |
} | |
} | |
protected void appendArray(String sql, Object[] arrayArg) { | |
StringBuilder marks = new StringBuilder(); | |
for (int i = 0; i < arrayArg.length; i++) { | |
if (i > 0) marks.append(','); | |
marks.append('?'); | |
} | |
queryParts.add(sql + marks); | |
bindValues.addAll(Arrays.asList(arrayArg)); | |
} | |
public Duck debug() { | |
logger.info("{" + Duck.class.getSimpleName() + "}\n" + this); | |
return this; | |
} | |
@Override | |
public String toString() { | |
return "SQL: " + getSql() + "\nParams: " + Arrays.toString(getParams()); | |
} | |
public String toSnakeCase(String str) { | |
if(str == null || str.isEmpty()) return str; | |
String res = str.replaceAll("([A-Z]+)", "_$1").toLowerCase(); | |
if (res.startsWith("_") && !str.startsWith("_")) { | |
res = res.substring(1); | |
} | |
return res; | |
} | |
public Map<String, Object> beanToMap(Object bean) { | |
try { | |
Map<String, Object> result = new HashMap<>(); | |
PropertyDescriptor[] props = Introspector.getBeanInfo(bean.getClass(), Object.class).getPropertyDescriptors(); | |
for (PropertyDescriptor pd : props) { | |
Method getter = pd.getReadMethod(); // getXxx() | |
if(getter != null) { | |
result.put(pd.getName(), getter.invoke(bean)); | |
} | |
} | |
return result; | |
} | |
catch (Exception ex) { | |
throw new DuckException(ex); | |
} | |
} | |
public SimpleEntry<String, List<Object>> pairInsert(Map<String, Object> params) { | |
SimpleEntry<List<String>, List<Object>> kv = pair(params); | |
String sql = String.join(", ", kv.getKey()); | |
return new SimpleEntry<>(sql, kv.getValue()); | |
} | |
public SimpleEntry<String, Object[]> pairUpdate(Map<String, Object> params) { | |
SimpleEntry<List<String>, List<Object>> kv = pair(params); | |
String sql = kv.getKey().stream() | |
.map(k -> String.format("%s = ?", k)) | |
.collect(Collectors.joining(", ")); | |
return new SimpleEntry<>(sql, kv.getValue().toArray()); | |
} | |
public SimpleEntry<List<String>, List<Object>> pair(Map<String, Object> params) { | |
List<String> keys = new ArrayList<>(params.size()); | |
List<Object> values = new ArrayList<>(params.size()); | |
int index = 0; | |
for (Map.Entry<String, Object> kv : params.entrySet()) { | |
String key = escape(isSnakeCase ? toSnakeCase(kv.getKey()) : kv.getKey()); | |
keys.add(index, key); | |
values.add(index, kv.getValue()); | |
index++; | |
} | |
return new SimpleEntry<>(keys, values); | |
} | |
@FunctionalInterface | |
protected interface ConnectionCallback<T> { | |
T doInConnection(Connection conn) throws SQLException; | |
} | |
public static class DuckException extends RuntimeException { | |
public DuckException(String message) { | |
super(message); | |
} | |
public DuckException(Throwable cause) { | |
super(cause); | |
} | |
public DuckException(String message, Throwable cause) { | |
super(message, cause); | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment