forked from plusone/simple-jdbc
完全移除对外部工具库 plusone-commons 和 Guava 的编译期依赖, 将所需功能内化实现,使项目成为真正的零依赖轻量级 JDBC 封装库。 - 新增 AssertTools: 断言工具(checkArgument / checkNotNull / checkState / checkCondition) - 新增 NamingTools: 命名转换(camelToSnake) - 新增 ThrowingConsumer / ThrowingPredicate: 可抛受检异常的函数式接口 - 新增 AssertToolsTests: 完整覆盖断言工具所有方法及边界场景 - pom.xml 移除 plusone-dependencies BOM,直接声明 jsr305 / test 依赖版本 - DefaultBeanRowMapper 使用 NamingTools.camelToSnake 替代 Guava CaseFormat - ParamBuilder 内联 Optional 展开逻辑,去除 OptionalTools / CollectionTools - JdbcOperationSupport 用 null/长度判断替代 ArrayTools.isEmpty/isNotEmpty - NOTICE 移除第三方依赖声明;README 移除无依赖分支说明 - 测试:新增缩写映射(URL/XML/ID/HTML/HTTP)覆盖、TransactionException 构造器测试 BREAKING CHANGE: BatchUpdateStatus 不再实现 IWithIntCode 接口; plusone-commons 和 Guava 不再作为传递依赖提供。
428 lines
15 KiB
Java
428 lines
15 KiB
Java
/*
|
||
* Copyright 2026-present ZhouXY
|
||
*
|
||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||
* you may not use this file except in compliance with the License.
|
||
* You may obtain a copy of the License at
|
||
*
|
||
* https://www.apache.org/licenses/LICENSE-2.0
|
||
*
|
||
* Unless required by applicable law or agreed to in writing, software
|
||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||
* See the License for the specific language governing permissions and
|
||
* limitations under the License.
|
||
*/
|
||
|
||
package xyz.zhouxy.jdbc;
|
||
|
||
import static xyz.zhouxy.jdbc.util.AssertTools.checkArgument;
|
||
import static xyz.zhouxy.jdbc.util.AssertTools.checkArgumentNotNull;
|
||
|
||
import java.sql.BatchUpdateException;
|
||
import java.sql.Connection;
|
||
import java.sql.PreparedStatement;
|
||
import java.sql.ResultSet;
|
||
import java.sql.SQLException;
|
||
import java.sql.Statement;
|
||
import java.sql.Types;
|
||
import java.time.Instant;
|
||
import java.time.LocalDate;
|
||
import java.time.LocalDateTime;
|
||
import java.time.LocalTime;
|
||
import java.util.Arrays;
|
||
import java.util.Collection;
|
||
import java.util.List;
|
||
|
||
import javax.annotation.Nonnull;
|
||
import javax.annotation.Nullable;
|
||
|
||
/**
|
||
* JdbcOperationSupport
|
||
*
|
||
* <p>
|
||
* 提供静态方法,封装 JDBC 基础操作
|
||
* </p>
|
||
*
|
||
* @author ZhouXY
|
||
* @since 1.0.0
|
||
*/
|
||
class JdbcOperationSupport {
|
||
|
||
// #region - query
|
||
|
||
/**
|
||
* 表示无法获取所更新的行数
|
||
*/
|
||
public static final int UNKNOWN_COUNT = -999;
|
||
|
||
/**
|
||
* 执行查询,并按照自定义处理逻辑对结果进行处理,将结果转换为指定类型并返回
|
||
*
|
||
* @param conn 数据库连接
|
||
* @param sql SQL
|
||
* @param params 参数
|
||
* @param resultHandler 结果处理器,用于处理 {@link ResultSet}
|
||
*/
|
||
static <T> T query(Connection conn, String sql, Object[] params, ResultHandler<T> resultHandler)
|
||
throws SQLException {
|
||
assertConnectionNotNull(conn);
|
||
assertSqlNotNull(sql);
|
||
assertResultHandlerNotNull(resultHandler);
|
||
return queryInternal(conn, sql, params, resultHandler);
|
||
}
|
||
|
||
// #endregion
|
||
|
||
// #region - queryList
|
||
|
||
/**
|
||
* 执行查询,将查询结果的每一行数据按照指定逻辑进行处理,返回结果列表
|
||
*
|
||
* @param conn 数据库连接
|
||
* @param sql SQL
|
||
* @param params 参数
|
||
* @param rowMapper {@link ResultSet} 中每一行的数据的处理逻辑
|
||
*/
|
||
static <T> List<T> queryList(Connection conn, String sql, Object[] params, RowMapper<T> rowMapper)
|
||
throws SQLException {
|
||
assertConnectionNotNull(conn);
|
||
assertSqlNotNull(sql);
|
||
assertRowMapperNotNull(rowMapper);
|
||
return queryListInternal(conn, sql, params, rowMapper);
|
||
}
|
||
|
||
/**
|
||
* 执行查询,返回结果映射为指定的类型。当结果为单列时使用
|
||
*
|
||
* @param conn 数据库连接
|
||
* @param sql SQL
|
||
* @param params 参数
|
||
* @param clazz 将结果映射为指定的类型
|
||
*/
|
||
static <T> List<T> queryList(Connection conn, String sql, Object[] params, Class<T> clazz)
|
||
throws SQLException {
|
||
assertConnectionNotNull(conn);
|
||
assertSqlNotNull(sql);
|
||
assertClazzNotNull(clazz);
|
||
return queryListInternal(conn, sql, params, (rs, rowNumber) -> rs.getObject(1, clazz));
|
||
}
|
||
|
||
// #endregion
|
||
|
||
// #region - queryFirst
|
||
|
||
/**
|
||
* 执行查询,将查询结果的第一行数据按照指定逻辑进行映射
|
||
*
|
||
* @param conn 数据库连接
|
||
* @param sql SQL
|
||
* @param params 参数
|
||
* @param rowMapper {@link ResultSet} 中每一行的数据的处理逻辑
|
||
*/
|
||
static <T> T queryFirst(Connection conn, String sql, Object[] params, RowMapper<T> rowMapper)
|
||
throws SQLException {
|
||
assertConnectionNotNull(conn);
|
||
assertSqlNotNull(sql);
|
||
assertRowMapperNotNull(rowMapper);
|
||
return queryFirstInternal(conn, sql, params, rowMapper);
|
||
}
|
||
|
||
/**
|
||
* 查询第一行第一列,并转换为指定类型
|
||
*
|
||
* @param <T> 目标类型
|
||
* @param sql SQL
|
||
* @param params 参数
|
||
* @param clazz 目标类型
|
||
*/
|
||
static <T> T queryFirst(Connection conn, String sql, Object[] params, Class<T> clazz)
|
||
throws SQLException {
|
||
assertConnectionNotNull(conn);
|
||
assertSqlNotNull(sql);
|
||
assertClazzNotNull(clazz);
|
||
return queryFirstInternal(conn, sql, params, (rs, rowNumber) -> rs.getObject(1, clazz));
|
||
}
|
||
|
||
// #endregion
|
||
|
||
// #region - update & batchUpdate
|
||
|
||
/**
|
||
* 执行更新操作
|
||
*
|
||
* @param conn 数据库连接
|
||
* @param sql 要执行的 SQL
|
||
* @param params 参数
|
||
* @return 更新记录数
|
||
*/
|
||
static int update(Connection conn, String sql, Object[] params)
|
||
throws SQLException {
|
||
assertConnectionNotNull(conn);
|
||
assertSqlNotNull(sql);
|
||
if (params != null && params.length > 0) {
|
||
try (PreparedStatement stmt = conn.prepareStatement(sql)) {
|
||
fillStatement(stmt, params);
|
||
return stmt.executeUpdate();
|
||
}
|
||
}
|
||
else {
|
||
try (Statement stmt = conn.createStatement()) {
|
||
return stmt.executeUpdate(sql);
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 执行 SQL 并返回生成的 keys
|
||
*
|
||
* @param conn 数据库连接
|
||
* @param sql 要执行的 SQL
|
||
* @param params 参数
|
||
* @param rowMapper 行数据映射逻辑
|
||
*
|
||
* @return generated keys
|
||
* @throws SQLException 数据库执行异常
|
||
*/
|
||
static <T> List<T> updateAndReturnKeys(Connection conn, String sql, Object[] params, RowMapper<T> rowMapper)
|
||
throws SQLException {
|
||
assertConnectionNotNull(conn);
|
||
assertSqlNotNull(sql);
|
||
assertRowMapperNotNull(rowMapper);
|
||
if (params != null && params.length > 0) {
|
||
try (PreparedStatement stmt = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) {
|
||
fillStatement(stmt, params);
|
||
stmt.executeUpdate();
|
||
try (ResultSet generatedKeys = stmt.getGeneratedKeys()) {
|
||
final ResultHandler<List<T>> resultHandler = ResultHandler.mapToList(rowMapper);
|
||
return resultHandler.handle(generatedKeys);
|
||
}
|
||
}
|
||
}
|
||
else {
|
||
try (Statement stmt = conn.createStatement()) {
|
||
stmt.executeUpdate(sql, Statement.RETURN_GENERATED_KEYS);
|
||
try (ResultSet generatedKeys = stmt.getGeneratedKeys()) {
|
||
final ResultHandler<List<T>> resultHandler = ResultHandler.mapToList(rowMapper);
|
||
return resultHandler.handle(generatedKeys);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 批量更新
|
||
*
|
||
* <p>
|
||
* 当无法获取所更新的行数时,对应位置的更新行数将被设置为 {@link #UNKNOWN_COUNT}。
|
||
*
|
||
* @param conn 数据库连接
|
||
* @param sql sql语句
|
||
* @param params 参数列表
|
||
* @param batchSize 每次批量更新的数据量
|
||
* @param quietly 静默分批更新。
|
||
* 如果 {@code quietly} 为 {@code true},分批更新过程中发生异常不中断操作;
|
||
* 如果 {@code quietly} 为 {@code false},分批更新过程中发生异常即中断操作,并返回结果。
|
||
*/
|
||
static BatchUpdateResult batchUpdate(Connection conn,
|
||
String sql, @Nullable Collection<Object[]> params, int batchSize,
|
||
boolean quietly)
|
||
throws SQLException {
|
||
assertConnectionNotNull(conn);
|
||
assertSqlNotNull(sql);
|
||
checkArgument(batchSize > 0, "The batch size must be greater than 0.");
|
||
if (params == null || params.isEmpty()) {
|
||
return new BatchUpdateResult(0, 0, batchSize);
|
||
}
|
||
|
||
final int paramsSize = params.size();
|
||
final int batchCount = (paramsSize + batchSize - 1) / batchSize;
|
||
|
||
final BatchUpdateResult result = new BatchUpdateResult(paramsSize, batchCount, batchSize);
|
||
|
||
try (PreparedStatement stmt = conn.prepareStatement(sql)) {
|
||
// 表示第几条数据,1, 2, 3, ..., paramsSize
|
||
int itemIndex = 0;
|
||
// 表示第几个批次,0, 1, ..., batchCount-1
|
||
int batchIndex = 0;
|
||
|
||
for (Object[] ps : params) {
|
||
itemIndex++;
|
||
fillStatement(stmt, ps);
|
||
stmt.addBatch();
|
||
|
||
// 表示当前数据在批次中的索引,1, 2, 3, ..., batchSize-1, batchSize
|
||
final int indexInBatch = (itemIndex - 1) % batchSize + 1;
|
||
|
||
if (indexInBatch == batchSize || itemIndex == paramsSize) {
|
||
try {
|
||
int[] updateCounts = stmt.executeBatch();
|
||
result.recordSuccessBatch(batchIndex, updateCounts);
|
||
}
|
||
catch (Exception e) {
|
||
final int[] updateCounts = getUpdateCountsOnError(indexInBatch, e);
|
||
result.recordErrorBatch(batchIndex, updateCounts, e);
|
||
if (!quietly) {
|
||
result.interrupt();
|
||
break;
|
||
}
|
||
}
|
||
finally {
|
||
stmt.clearBatch();
|
||
batchIndex++;
|
||
}
|
||
}
|
||
}
|
||
return result;
|
||
}
|
||
}
|
||
|
||
private static int[] getUpdateCountsOnError(final int indexInBatch, final Exception e) {
|
||
final int[] updateCounts;
|
||
if (e instanceof BatchUpdateException) {
|
||
updateCounts = ((BatchUpdateException) e).getUpdateCounts();
|
||
}
|
||
else {
|
||
updateCounts = new int[indexInBatch];
|
||
Arrays.fill(updateCounts, UNKNOWN_COUNT);
|
||
}
|
||
return updateCounts;
|
||
}
|
||
|
||
// #endregion
|
||
|
||
// #region - internal
|
||
|
||
/**
|
||
* 执行查询,将查询结果按照指定逻辑进行处理并返回
|
||
*
|
||
* @param conn 数据库连接
|
||
* @param sql SQL
|
||
* @param params 参数
|
||
* @param resultHandler 结果处理器,用于处理 {@link ResultSet}
|
||
*/
|
||
private static <T> T queryInternal(@Nonnull Connection conn,
|
||
@Nonnull String sql,
|
||
@Nullable Object[] params,
|
||
@Nonnull ResultHandler<T> resultHandler)
|
||
throws SQLException {
|
||
if (params != null && params.length > 0) {
|
||
try (PreparedStatement stmt = createPreparedStatementInternal(conn, sql, params);
|
||
ResultSet rs = stmt.executeQuery()) {
|
||
return resultHandler.handle(rs);
|
||
}
|
||
}
|
||
else {
|
||
try (Statement stmt = conn.createStatement();
|
||
ResultSet rs = stmt.executeQuery(sql)) {
|
||
return resultHandler.handle(rs);
|
||
}
|
||
}
|
||
}
|
||
|
||
private static PreparedStatement createPreparedStatementInternal(
|
||
@Nonnull Connection conn,
|
||
@Nonnull String sql,
|
||
@Nullable Object[] params)
|
||
throws SQLException {
|
||
PreparedStatement stmt = conn.prepareStatement(sql);
|
||
fillStatement(stmt, params);
|
||
return stmt;
|
||
}
|
||
|
||
/**
|
||
* 执行查询,将查询结果的每一行数据按照指定逻辑进行处理,返回结果列表
|
||
*
|
||
* @param conn 数据库连接
|
||
* @param sql SQL
|
||
* @param params 参数
|
||
* @param rowMapper {@link ResultSet} 中每一行的数据的处理逻辑
|
||
*/
|
||
private static <T> List<T> queryListInternal(@Nonnull Connection conn,
|
||
@Nonnull String sql,
|
||
@Nullable Object[] params,
|
||
@Nonnull RowMapper<T> rowMapper)
|
||
throws SQLException {
|
||
return queryInternal(conn, sql, params, ResultHandler.mapToList(rowMapper));
|
||
}
|
||
|
||
/**
|
||
* 执行查询,将查询结果的第一行数据按照指定逻辑进行处理,返回映射结果
|
||
*
|
||
* @param conn 数据库连接
|
||
* @param sql SQL
|
||
* @param params 参数
|
||
* @param rowMapper 行数据映射逻辑
|
||
* @return 映射结果。如果查询结果为空,则返回 null
|
||
*/
|
||
private static <T> T queryFirstInternal(@Nonnull Connection conn,
|
||
@Nonnull String sql,
|
||
@Nullable Object[] params,
|
||
@Nonnull RowMapper<T> rowMapper)
|
||
throws SQLException {
|
||
return queryInternal(conn, sql, params, rs ->
|
||
rs.next() ? rowMapper.mapRow(rs, 0) : null);
|
||
}
|
||
|
||
// #endregion
|
||
|
||
/**
|
||
* 填充参数
|
||
*/
|
||
private static void fillStatement(@Nonnull PreparedStatement stmt, @Nullable Object[] params)
|
||
throws SQLException {
|
||
if (params != null && params.length > 0) {
|
||
Object param;
|
||
for (int i = 0; i < params.length; i++) {
|
||
param = params[i];
|
||
if (param == null) {
|
||
stmt.setObject(i + 1, null, Types.NULL);
|
||
}
|
||
else if (param instanceof LocalDate) {
|
||
stmt.setDate(i + 1, java.sql.Date.valueOf((LocalDate) param));
|
||
}
|
||
else if (param instanceof LocalTime) {
|
||
stmt.setTime(i + 1, java.sql.Time.valueOf((LocalTime) param));
|
||
}
|
||
else if (param instanceof LocalDateTime) {
|
||
stmt.setTimestamp(i + 1, java.sql.Timestamp.valueOf((LocalDateTime) param));
|
||
}
|
||
else if (param instanceof Instant) {
|
||
stmt.setTimestamp(i + 1, java.sql.Timestamp.from((Instant) param));
|
||
}
|
||
else {
|
||
stmt.setObject(i + 1, param);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// #region - 参数校验
|
||
|
||
private static void assertConnectionNotNull(Connection conn) {
|
||
checkArgumentNotNull(conn, "The argument \"conn\" could not be null.");
|
||
}
|
||
|
||
private static void assertSqlNotNull(String sql) {
|
||
checkArgumentNotNull(sql, "The argument \"sql\" could not be null.");
|
||
}
|
||
|
||
private static void assertRowMapperNotNull(RowMapper<?> rowMapper) {
|
||
checkArgumentNotNull(rowMapper, "The argument \"rowMapper\" could not be null.");
|
||
}
|
||
|
||
private static void assertResultHandlerNotNull(ResultHandler<?> resultHandler) {
|
||
checkArgumentNotNull(resultHandler, "The argument \"resultHandler\" could not be null.");
|
||
}
|
||
|
||
private static void assertClazzNotNull(Class<?> clazz) {
|
||
checkArgumentNotNull(clazz, "The argument \"clazz\" could not be null.");
|
||
}
|
||
|
||
// #endregion
|
||
|
||
private JdbcOperationSupport() {
|
||
throw new IllegalStateException("Utility class");
|
||
}
|
||
}
|