Compare commits

...

22 Commits

Author SHA1 Message Date
cedf1966ba build: 保持开发分支的版本后缀为 SNAPSHOT 2025-09-03 15:55:31 +08:00
20edb8f21a chore: add license header 2025-08-26 09:30:52 +08:00
e0c8b0d46e docs: 更新 author 信息 2025-07-29 17:08:57 +08:00
d21a935647 docs: 更新项目文档与配置 (#1 from gitee)
* docs(README): 更新项目文档
* chore: 修改 Markdown 文件的缩进设置
* chore: 更新 .gitignore
* chore: 删除 IntelliJ IDEA 的配置
2025-07-29 09:00:23 +00:00
94e38d062d style: 优化测试数据库表的创建语句格式 (plusone/simple-jdbc#8 from gitea)
- 使用反引号括起字段名
- 调整字段间的空格和换行

Signed-off-by: ZhouXY108 <luquanlion@outlook.com>
2025-07-26 03:25:20 +08:00
15c8970522 build: 更新项目配置和依赖版本 (plusone/simple-jdbc#7 from gitea)
- 添加项目名称、描述和 URL
- 更新 plusone-dependencies 和 plusone-commons 依赖版本
2025-07-26 03:17:47 +08:00
748cf430b3 chore: 设置 SQL 文件缩进为 2 个空格 (plusone/simple-jdbc#6 from gitea)
在 .editorconfig 文件中设置 SQL 文件的 indent_size 为 2
2025-07-26 03:04:01 +08:00
ad7320c280 docs: 优化 javadoc 2025-06-02 00:18:29 +08:00
973552b7d1 build: 简化 JUnit 依赖声明
Reviewed-on: http://zhouxy.xyz:3000/ZhouXY108/simple-jdbc/pulls/5
Co-authored-by: ZhouXY108 <luquanlion@outlook.com>
Co-committed-by: ZhouXY108 <luquanlion@outlook.com>
2025-05-28 20:28:51 +08:00
07b5199219 refactor: 重构单元测试中的表和数据的初始化
将表结构和初始数据的SQL放在 *.sql 文件中,使单元测试代码更清晰。

Reviewed-on: http://zhouxy.xyz:3000/ZhouXY108/simple-jdbc/pulls/4
Co-authored-by: ZhouXY108 <luquanlion@outlook.com>
Co-committed-by: ZhouXY108 <luquanlion@outlook.com>
2025-05-28 20:27:32 +08:00
0317a9e561 refactor: 将 DefaultBeanRowMapper 中的属性名称映射逻辑抽取到新的方法中 2025-05-28 17:30:28 +08:00
1f153510e6 docs: 修复 javadoc 的引用 2025-05-28 17:26:47 +08:00
f04a34f366 style: 格式化代码 2025-05-28 17:24:44 +08:00
7e2072df06 refactor: 简化代码
Co-authored-by: 周兴毅 <IAM_970924609@csair.com>
Reviewed-on: http://zhouxy.xyz:3000/ZhouXY108/simple-jdbc/pulls/3
2025-05-09 16:19:28 +08:00
2310173b84 test: 重构单元测试代码 2025-05-02 22:58:38 +08:00
ccddd0b610 build: 更新依赖
plusone-commons 更新至 1.1.0-SNAPSHOT;
junit 更新至 5.10.3;
补充 junit-jupiter-engine。
2025-05-02 22:58:27 +08:00
5d21f13757 重载 ParamBuilder#buildParams 方法
Co-authored-by: ZhouXY108 <luquanlion@outlook.com>
Co-committed-by: ZhouXY108 <luquanlion@outlook.com>
2025-04-05 02:16:21 +08:00
99bfde2b93 修改 plusone-commons 版本 2025-01-22 20:55:53 +08:00
feb421cfa0 1. 删除 DbRecord;2. plusone-commons 删除 SQL Builder。 2024-12-29 23:16:08 +08:00
ce86d97e38 新增查询 boolean 的方法,并简单重构代码。 2024-12-22 23:04:28 +08:00
01fda84276 将通用的对 JDBC 进行操作的静态方法封装到 JdbcOperationSupport;使用 JdbcOperations 定义 SimpleJdbcTemplate 和 JdbcExecutor 的行为。 2024-11-02 11:33:15 +08:00
6b5bcf0b5c 补充文档注释 2024-11-02 10:55:23 +08:00
21 changed files with 1740 additions and 806 deletions

View File

@@ -10,3 +10,9 @@ end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.sql]
indent_size = 2
[*.md]
indent_size = 2

5
.gitignore vendored
View File

@@ -4,10 +4,7 @@ target/
!**/src/test/**/target/
### IntelliJ IDEA ###
.idea/modules.xml
.idea/jarRepositories.xml
.idea/compiler.xml
.idea/libraries/
.idea/
*.iws
*.iml
*.ipr

3
.idea/.gitignore generated vendored
View File

@@ -1,3 +0,0 @@
# 默认忽略的文件
/shelf/
/workspace.xml

7
.idea/encodings.xml generated
View File

@@ -1,7 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Encoding">
<file url="file://$PROJECT_DIR$/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/src/main/resources" charset="UTF-8" />
</component>
</project>

View File

@@ -1,6 +0,0 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="NonSerializableWithSerialVersionUIDField" enabled="true" level="WARNING" enabled_by_default="true" />
</profile>
</component>

14
.idea/misc.xml generated
View File

@@ -1,14 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="MavenProjectsManager">
<option name="originalFiles">
<list>
<option value="$PROJECT_DIR$/pom.xml" />
</list>
</option>
</component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_8" default="true" project-jdk-name="1.8" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/out" />
</component>
</project>

6
.idea/vcs.xml generated
View File

@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

179
README.md
View File

@@ -1,6 +1,181 @@
# SimpleJDBC
对 JDBC 的简单封装。
之前遇到的一个老项目,没有引入任何 ORM 框架,使用的 JDK7 明明支持泛型,所依赖的 spring-jdbc 居然是没有泛型的远古版本,该项目又不允许随意添加依赖,对数据库的操作几乎都在写原生 JDBC。故自己写了几个工具类对 JDBC 进行简单封装,后来逐渐改进完善
之前遇到的一个老项目,没有引入任何 ORM 框架,对数据库的操作几乎都在写原生 JDBC。故自己写了几个工具类对 JDBC 进行简单封装。
本项目不比成熟的工具,如若使用请自行承担风险。建议仅作为 JDBC 的学习参考。
## 查询
### 查询方法
- `query`**最基础的查询方法**。可使用 `ResultHandler` 将查询结果映射为 Java 对象。
- `queryList`**查询列表**。可使用 `RowMapper` 将结果的每一行数据映射为 Java 对象,返回列表。
- `queryFirst`**查询,并获取第一行数据**。一般可以结合 `LIMIT 1` 使用。可使用 `RowMapper` 将结果的第一行数据映射为 Java 对象,返回 `Optional`
- `queryFirstXXX`**查询,并获取第一行数据的第一个字段**,并转换为对应类型,返回对应的类型的 `Optional`
- `queryAsBoolean`**查询,并获取第一行数据的第一个字段**,并转换为布尔类型。如果结果为空,则返回 `false`
### 结果映射
- `ResultHandler` 用于处理查询结果,自定义逻辑将完整的 `ResultSet` 映射为 Java 对象。 *结果可以是任意类型(包括集合)。*
- `RowMapper` 用于将 `ResultSet` 中的一行数据映射为 Java 对象。
- `RowMapper#HASH_MAP_MAPPER`:将 `ResultSet` 中的一行数据映射为 `HashMap`
- `RowMapper#beanRowMapper`:返回将 `ResultSet` 转换为 Java Bean 的默认实现。
## 更新
- `int update`**执行 DML**,包括 `INSERT``UPDATE``DELETE` 等。返回受影响行数。
- `<T> List<T> update`**执行 DML**,自动生成的字段将使用 `rowMapper` 进行映射,并返回列表。
- `List<int[]> batchUpdate`**分批次执行 DML**,返回每个批次的每条 SQL 语句影响的行数。
- `List<int[]> batchUpdateAndIgnoreException`**分批次执行 DML如果某个批次出现异常继续执行下一个批次**。返回每个批次的每条 SQL 语句影响的行数。
## 事务
- `executeTransaction`**执行事务**。传入一个 `ThrowingConsumer` 函数,入参是一个 `JdbcExecutor` 对象,在 `ThrowingConsumer` 内使用该入参执行 jdbc 操作。如果 `ThrowingConsumer` 内部有异常抛出,这些操作将被回滚。
- `commitIfTrue`**执行事务**。传入一个 `ThrowingPredicate` 函数,入参是一个 `JdbcExecutor` 对象,在 `ThrowingPredicate` 内使用该入参执行 jdbc 操作。如果`ThrowingPredicate` 返回 `true`,则提交事务;如果返回 `false` 或有异常抛出,则回滚这些操作。
## 参数构建
此项目中的所有查询和更新的方法,都**不使用可变长入参**,避免强行将 SQL 语句的参数列表放在最后,也避免和数组发生歧义。
### 构建参数列表
可使用 `ParamBuilder#buildParams` 构建 `Object[]` 数组作为 SQL 的参数列表。该方法会自动将 `Optional` 中的值“拆”出来。
### 批量构建参数列表
使用 `ParamBuilder#buildBatchParams`,将使用传入的函数,将集合中的每一个元素转为 `Object[]`,并返回一个 `List<Object[]>`
## 示例
创建 SimpleJdbcTemplate 对象
```java
SimpleJdbcTemplate jdbcTemplate = new SimpleJdbcTemplate(dataSource);
```
查询
```java
// 查询
List<Account> list = jdbcTemplate.query(
"SELECT * FROM account WHERE deleted = 0 AND username LIKE ? AND org_no = ?",
buildParams("admin%", "0000"),
rs -> {
List<T> result = new ArrayList<>();
while (rs.next()) {
result.add(new Account(
rs.getLong("id"),
rs.getString("username"),
rs.getString("password"),
rs.getString("org_no"),
rs.getTimestamp("create_time"),
rs.getTimestamp("update_time")
));
}
return result;
}
);
// 查询列表
List<Account> list = jdbcTemplate.queryList(
"SELECT * FROM account WHERE deleted = 0 AND username LIKE ? AND org_no = ?",
buildParams("admin%", "0000"),
(rs, rowNum) -> new Account(
rs.getLong("id"),
rs.getString("username"),
rs.getString("password"),
rs.getString("org_no"),
rs.getTimestamp("create_time"),
rs.getTimestamp("update_time")
)
);
// 查询一行数据
Optional<Account> account = jdbcTemplate.queryFirst(
"SELECT * FROM account WHERE deleted = 0 AND id = ?",
buildParams(10000L),
(rs, rowNum) -> new Account(
rs.getLong("id"),
rs.getString("username"),
rs.getString("password"),
rs.getString("org_no"),
rs.getTimestamp("create_time"),
)
)
// 查询一行数据,并获取第一个字段
OptionalInt age = jdbcTemplate.queryFirstInt(
"SELECT age FROM view_account WHERE deleted = 0 AND id = ?",
buildParams(10000L)
);
// 查询 boolean
boolean exists = jdbcTemplate.queryAsBoolean(
"SELECT EXISTS(SELECT 1 FROM account WHERE deleted = 0 AND id = ? LIMIT 1)",
buildParams(10000L)
);
```
更新
```java
// 执行 DML
int affectedRows = jdbcTemplate.update(
"UPDATE account SET deleted = 1 WHERE id = ?",
buildParams(10000L)
);
// 执行 DML并获取生成的主键
List<Pair<Long, LocalDateTime>> keys = jdbcTemplate.update(
"INSERT INTO account (username, password, org_no) VALUES (?, ?, ?)",
buildParams("admin", "123456", "0000"),
(rs, rowNum) -> Pair.of(
rs.getLong("id"),
rs.getObject("create_time", LocalDateTime.class)
)
);
```
批量更新
```java
jdbcTemplate.batchUpdate(
"INSERT INTO account (username, password, org_no) VALUES (?, ?, ?)",
buildBatchParams(accountList, account -> buildParams(
account.getUsername(),
account.getPassword(),
account.getOrgNo()
)),
100 // 每100条数据一个批次
);
```
事务
```java
jdbcTemplate.executeTransaction(jdbc -> {
...
jdbc.update(...);
...
jdbc.update(...);
...
});
jdbcTemplate.commitIfTrue(jdbc -> {
...
jdbc.update(...);
...
if (...) {
// 中断操作并回滚
return false;
}
...
if (...) {
// 某些条件下提前结束并提交事务
return true;
}
...
jdbc.update(...);
...
// 提交事务
return true;
});
```
>**!!!本项目不比成熟的工具,如若使用请自行承担风险。建议仅作为 JDBC 的学习参考。**

27
pom.xml
View File

@@ -6,32 +6,48 @@
<groupId>xyz.zhouxy.jdbc</groupId>
<artifactId>simple-jdbc</artifactId>
<version>1.0.0-alpha</version>
<version>1.0.0-SNAPSHOT</version>
<name>Simple JDBC</name>
<description>对 JDBC 的简单封装。</description>
<url>http://gitea.zhouxy.xyz/plusone/simple-jdbc</url>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<plusone-commons.version>1.1.0-SNAPSHOT</plusone-commons.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>xyz.zhouxy.plusone</groupId>
<artifactId>plusone-dependencies</artifactId>
<version>${plusone-commons.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>xyz.zhouxy.plusone</groupId>
<artifactId>plusone-commons</artifactId>
<version>1.0.0-alpha</version>
<version>${plusone-commons.version}</version>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>5.9.2</version>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.2.11</version>
<scope>test</scope>
</dependency>
@@ -39,7 +55,6 @@
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>2.2.224</version>
<scope>test</scope>
</dependency>
</dependencies>

View File

@@ -1,77 +0,0 @@
/*
* Copyright 2022-2024 the original author or authors.
*
* 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 com.google.common.annotations.Beta;
import com.google.common.base.Preconditions;
import xyz.zhouxy.plusone.commons.collection.AbstractMapWrapper;
import xyz.zhouxy.plusone.commons.util.OptionalTools;
import xyz.zhouxy.plusone.commons.util.StringTools;
import java.util.*;
@Beta
public class DbRecord extends AbstractMapWrapper<String, Object, DbRecord> {
public DbRecord() {
super(new HashMap<>(), k -> Preconditions.checkArgument(StringTools.isNotBlank(k), "Key must has text."), null);
}
public DbRecord(Map<String, Object> map) {
super(map, k -> Preconditions.checkArgument(StringTools.isNotBlank(k), "Key must has text."), null);
}
public Optional<String> getValueAsString(String key) {
return this.getAndConvert(key);
}
public <T> List<T> getValueAsList(String key) {
return this.<Collection<T>>getAndConvert(key)
.map(l -> (l instanceof List) ? (List<T>) l : new ArrayList<>(l))
.orElse(Collections.emptyList());
}
public <T> Set<T> getValueAsSet(String key) {
return this.<Collection<T>>getAndConvert(key)
.map(l -> (l instanceof Set) ? (Set<T>) l : new HashSet<>(l))
.orElse(Collections.emptySet());
}
public OptionalInt getValueAsInt(String key) {
return OptionalTools.toOptionalInt(this.getAndConvert(key));
}
public OptionalLong getValueAsLong(String key) {
return OptionalTools.toOptionalLong(this.getAndConvert(key));
}
public OptionalDouble getValueAsDouble(String key) {
return OptionalTools.toOptionalDouble(this.getAndConvert(key));
}
@Override
protected DbRecord getSelf() {
return this;
}
private static final String STR_PREFIX = DbRecord.class.getName() + '@';
@Override
public String toString() {
return STR_PREFIX + super.toString();
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2022-2024 the original author or authors.
* Copyright 2022-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -35,9 +35,28 @@ import javax.annotation.Nullable;
import com.google.common.base.CaseFormat;
import xyz.zhouxy.plusone.commons.annotation.StaticFactoryMethod;
/**
* DefaultBeanRowMapper
*
* <p>
* 默认实现的将 {@link ResultSet} 转换为 Java Bean 的 {@link RowMapper}。
* </p>
*
* <p>
* <b>NOTE: 使用反射获取类型信息,也是使用反射调用无参构造器和 {@code setter} 方法。
* 实际使用中还是建议针对目标类型自定义 {@link RowMapper}。</b>
* </p>
* @author ZhouXY108 <luquanlion@outlook.com>
* @since 1.0.0
*/
public class DefaultBeanRowMapper<T> implements RowMapper<T> {
/** Bean 的无参构造器 */
private final Constructor<T> constructor;
/** 列名到属性的映射 */
private final Map<String, PropertyDescriptor> colPropertyMap;
private DefaultBeanRowMapper(Constructor<T> constructor, Map<String, PropertyDescriptor> colPropertyMap) {
@@ -45,10 +64,29 @@ public class DefaultBeanRowMapper<T> implements RowMapper<T> {
this.colPropertyMap = colPropertyMap;
}
/**
* 创建一个 {@code DefaultBeanRowMapper}
*
* @param <T> Bean 类型
* @param beanType Bean 类型
* @return DefaultBeanRowMapper 对象
* @throws SQLException 创建 {@code DefaultBeanRowMapper} 出现错误的异常时抛出
*/
@StaticFactoryMethod(DefaultBeanRowMapper.class)
public static <T> DefaultBeanRowMapper<T> of(Class<T> beanType) throws SQLException {
return of(beanType, null);
}
/**
* 创建一个 {@code DefaultBeanRowMapper}
*
* @param <T> Bean 类型
* @param beanType Bean 类型
* @param propertyColMap Bean 字段与列名的映射关系。key 是字段value 是列名。
* @return {@code DefaultBeanRowMapper} 对象
* @throws SQLException 创建 {@code DefaultBeanRowMapper} 出现错误的异常时抛出
*/
@StaticFactoryMethod(DefaultBeanRowMapper.class)
public static <T> DefaultBeanRowMapper<T> of(Class<T> beanType, @Nullable Map<String, String> propertyColMap)
throws SQLException {
try {
@@ -56,24 +94,7 @@ public class DefaultBeanRowMapper<T> implements RowMapper<T> {
Constructor<T> constructor = beanType.getDeclaredConstructor();
constructor.setAccessible(true); // NOSONAR
// 构建 column name 和 PropertyDescriptor 的 映射
BeanInfo beanInfo = Introspector.getBeanInfo(beanType);
PropertyDescriptor[] propertyDescriptors = beanInfo.getPropertyDescriptors();
Function<? super PropertyDescriptor, String> keyMapper;
if (propertyColMap == null || propertyColMap.isEmpty()) {
keyMapper = p -> CaseFormat.LOWER_CAMEL.to(CaseFormat.LOWER_UNDERSCORE, p.getName());
}
else {
keyMapper = p -> {
String propertyName = p.getName();
String colName = propertyColMap.get(propertyName);
return colName != null ? colName
: CaseFormat.LOWER_CAMEL.to(CaseFormat.LOWER_UNDERSCORE, propertyName);
};
}
Map<String, PropertyDescriptor> colPropertyMap = Arrays.stream(propertyDescriptors).collect(
Collectors.toMap(keyMapper, Function.identity(), (a, b) -> b));
final Map<String, PropertyDescriptor> colPropertyMap = buildColPropertyMap(beanType, propertyColMap);
return new DefaultBeanRowMapper<>(constructor, colPropertyMap);
}
catch (IntrospectionException e) {
@@ -84,13 +105,17 @@ public class DefaultBeanRowMapper<T> implements RowMapper<T> {
}
}
/** {@inheritDoc} */
@Override
public T mapRow(ResultSet rs, int rowNumber) throws SQLException {
try {
// 调用无参构造器创建实例
T newInstance = this.constructor.newInstance();
ResultSetMetaData metaData = rs.getMetaData();
// 遍历结果的每一列
for (int i = 1; i <= metaData.getColumnCount(); i++) {
String colName = metaData.getColumnName(i);
// 获取查询结果列名对应的属性,调用 setter
PropertyDescriptor propertyDescriptor = this.colPropertyMap.get(colName);
if (propertyDescriptor != null) {
Method setter = propertyDescriptor.getWriteMethod();
@@ -107,4 +132,36 @@ public class DefaultBeanRowMapper<T> implements RowMapper<T> {
throw new SQLException(e);
}
}
/**
* 构建 column name 和 PropertyDescriptor 的 映射
*
* @param <T> Java bean 类型
* @param beanType Java bean 类型
* @param propertyColMap 属性与列名的映射
* @return column name 和 PropertyDescriptor 的映射
* @throws IntrospectionException if an exception occurs during introspection.
*/
private static <T> Map<String, PropertyDescriptor> buildColPropertyMap(
Class<T> beanType, Map<String, String> propertyColMap) throws IntrospectionException {
BeanInfo beanInfo = Introspector.getBeanInfo(beanType);
PropertyDescriptor[] propertyDescriptors = beanInfo.getPropertyDescriptors();
// Bean 的属性名为小驼峰,对应的列名为下划线
Function<? super PropertyDescriptor, String> keyMapper;
if (propertyColMap == null || propertyColMap.isEmpty()) {
keyMapper = p -> CaseFormat.LOWER_CAMEL.to(CaseFormat.LOWER_UNDERSCORE, p.getName());
}
else {
keyMapper = p -> {
String propertyName = p.getName();
String colName = propertyColMap.get(propertyName);
return colName != null ? colName
: CaseFormat.LOWER_CAMEL.to(CaseFormat.LOWER_UNDERSCORE, propertyName);
};
}
return Arrays.stream(propertyDescriptors)
.collect(Collectors.toMap(keyMapper, Function.identity(), (a, b) -> b));
}
}

View File

@@ -0,0 +1,478 @@
/*
* Copyright 2024-2025 the original author or authors.
*
* 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 java.math.BigDecimal;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import com.google.common.collect.Lists;
import xyz.zhouxy.plusone.commons.collection.CollectionTools;
import xyz.zhouxy.plusone.commons.util.AssertTools;
/**
* JdbcOperationSupport
*
* <p>
* 提供静态方法,封装 JDBC 基础操作
* </p>
*
* @author ZhouXY108 <luquanlion@outlook.com>
* @since 1.0.0
*/
class JdbcOperationSupport {
// #region - query
/**
* 执行查询,并按照自定义处理逻辑对结果进行处理,将结果转换为指定类型并返回
*
* @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));
}
/**
* 查询第一行第一列,并转换为字符串
*
* @param conn 数据库连接
* @param sql SQL
* @param params 参数
*/
static String queryFirstString(Connection conn, String sql, Object[] params)
throws SQLException {
return queryFirst(conn, sql, params, (rs, rowNumber) -> rs.getString(1));
}
/**
* 查询第一行第一列,并转换为整数值
*
* @param conn 数据库连接
* @param sql SQL
* @param params 参数
*/
static Integer queryFirstInt(Connection conn, String sql, Object[] params)
throws SQLException {
return queryFirst(conn, sql, params, (rs, rowNumber) -> rs.getInt(1));
}
/**
* 查询第一行第一列,并转换为长整型
*
* @param conn 数据库连接
* @param sql SQL
* @param params 参数
*/
static Long queryFirstLong(Connection conn, String sql, Object[] params)
throws SQLException {
return queryFirst(conn, sql, params, (rs, rowNumber) -> rs.getLong(1));
}
/**
* 查询第一行第一列,并转换为双精度浮点型
*
* @param conn 数据库连接
* @param sql SQL
* @param params 参数
*/
static Double queryFirstDouble(Connection conn, String sql, Object[] params)
throws SQLException {
return queryFirst(conn, sql, params, (rs, rowNumber) -> rs.getDouble(1));
}
/**
* 查询第一行第一列,并转换为 {@link BigDecimal}
*
* @param conn 数据库连接
* @param sql SQL
* @param params 参数
*/
static BigDecimal queryFirstBigDecimal(Connection conn, String sql, Object[] params)
throws SQLException {
return queryFirst(conn, sql, params, (rs, rowNumber) -> rs.getBigDecimal(1));
}
/**
* 查询结果,并转换为 bool 值
*
* @param conn 数据库连接
* @param sql SQL
* @param params 参数
*/
static Boolean queryFirstBoolean(Connection conn, String sql, Object[] params)
throws SQLException {
return queryFirst(conn, sql, params, (rs, rowNumber) -> rs.getBoolean(1));
}
// #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);
try (PreparedStatement stmt = conn.prepareStatement(sql)) {
fillStatement(stmt, params);
return stmt.executeUpdate();
}
}
/**
* 执行 SQL 并返回生成的 keys
*
* @param conn 数据库连接
* @param sql 要执行的 SQL
* @param params 参数
* @param rowMapper 行数据映射逻辑
*
* @return generated keys
* @throws SQLException 执行 SQL 遇到异常情况将抛出
*/
static <T> List<T> update(Connection conn, String sql, Object[] params, RowMapper<T> rowMapper)
throws SQLException {
assertConnectionNotNull(conn);
assertSqlNotNull(sql);
assertRowMapperNotNull(rowMapper);
final List<T> result = new ArrayList<>();
try (PreparedStatement stmt = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) {
fillStatement(stmt, params);
stmt.executeUpdate();
try (ResultSet generatedKeys = stmt.getGeneratedKeys()) {
int rowNumber = 0;
while (generatedKeys.next()) {
T e = rowMapper.mapRow(generatedKeys, rowNumber++);
result.add(e);
}
}
return result;
}
}
/**
* 执行批量更新,批量更新数据,返回每条记录更新的行数
*
* @param conn 数据库连接
* @param sql SQL 语句
* @param params 参数列表
* @param batchSize 每次批量更新的数据量
*/
static List<int[]> batchUpdate(Connection conn, String sql, Collection<Object[]> params, int batchSize)
throws SQLException {
assertConnectionNotNull(conn);
assertSqlNotNull(sql);
if (params == null || params.isEmpty()) {
return Collections.emptyList();
}
int executeCount = params.size() / batchSize;
executeCount = (params.size() % batchSize == 0) ? executeCount : (executeCount + 1);
List<int[]> result = Lists.newArrayListWithCapacity(executeCount);
try (PreparedStatement stmt = conn.prepareStatement(sql)) {
int i = 0;
for (Object[] ps : params) {
i++;
fillStatement(stmt, ps);
stmt.addBatch();
if (i % batchSize == 0 || i >= params.size()) {
int[] n = stmt.executeBatch();
result.add(n);
stmt.clearBatch();
}
}
return result;
}
}
/**
* 批量更新,返回更新成功的记录行数。发生异常时不中断操作,将异常存入 {@code exceptions} 中
*
* @param conn 数据库连接
* @param sql sql语句
* @param params 参数列表
* @param batchSize 每次批量更新的数据量
* @param exceptions 异常列表,用于记录异常信息
*/
static List<int[]> batchUpdateAndIgnoreException(Connection conn,
String sql, @Nullable Collection<Object[]> params, int batchSize,
List<Exception> exceptions)
throws SQLException {
assertConnectionNotNull(conn);
assertSqlNotNull(sql);
AssertTools.checkArgument(CollectionTools.isNotEmpty(exceptions),
"The list used to store exceptions should be non-null and empty.");
if (params == null || params.isEmpty()) {
return Collections.emptyList();
}
int executeCount = params.size() / batchSize;
executeCount = (params.size() % batchSize == 0) ? executeCount : (executeCount + 1);
List<int[]> result = Lists.newArrayListWithCapacity(executeCount);
try (PreparedStatement stmt = conn.prepareStatement(sql)) {
int i = 0;
for (Object[] ps : params) {
i++;
fillStatement(stmt, ps);
stmt.addBatch();
final int batchIndex = i % batchSize;
if (batchIndex == 0 || i >= params.size()) {
try {
int[] n = stmt.executeBatch();
result.add(n);
stmt.clearBatch();
}
catch (Exception e) {
int n = (i >= params.size() && batchIndex != 0) ? batchIndex : batchSize;
result.add(new int[n]);
stmt.clearBatch();
// 收集异常信息
exceptions.add(e);
}
}
}
return result;
}
}
// #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 {
try (PreparedStatement stmt = conn.prepareStatement(sql)) {
fillStatement(stmt, params);
try (ResultSet rs = stmt.executeQuery()) {
return resultHandler.handle(rs);
}
}
}
/**
* 执行查询,将查询结果的每一行数据按照指定逻辑进行处理,返回结果列表
*
* @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, rs -> {
List<T> result = new ArrayList<>();
int rowNumber = 0;
while (rs.next()) {
T e = rowMapper.mapRow(rs, rowNumber++);
result.add(e);
}
return result;
});
}
/**
* 执行查询,将查询结果的第一行数据按照指定逻辑进行处理,返回映射结果
*
* @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 instanceof java.sql.Date) {
stmt.setDate(i + 1, (java.sql.Date) param);
}
else if (param instanceof java.sql.Time) {
stmt.setTime(i + 1, (java.sql.Time) param);
}
else if (param instanceof java.sql.Timestamp) {
stmt.setTimestamp(i + 1, (java.sql.Timestamp) param);
}
else {
stmt.setObject(i + 1, param);
}
}
}
}
// #region - 参数校验
private static void assertConnectionNotNull(Connection conn) {
AssertTools.checkArgument(Objects.nonNull(conn),
"The argument \"conn\" could not be null.");
}
private static void assertSqlNotNull(String sql) {
AssertTools.checkArgument(Objects.nonNull(sql),
"The argument \"sql\" could not be null.");
}
private static void assertRowMapperNotNull(RowMapper<?> rowMapper) {
AssertTools.checkArgument(Objects.nonNull(rowMapper),
"The argument \"rowMapper\" could not be null.");
}
private static void assertResultHandlerNotNull(ResultHandler<?> resultHandler) {
AssertTools.checkArgument(Objects.nonNull(resultHandler),
"The argument \"resultHandler\" could not be null.");
}
private static void assertClazzNotNull(Class<?> clazz) {
AssertTools.checkArgument(Objects.nonNull(clazz),
"The argument \"clazz\" could not be null.");
}
// #endregion
private JdbcOperationSupport() {
throw new IllegalStateException("Utility class");
}
}

View File

@@ -0,0 +1,357 @@
/*
* Copyright 2024-2025 the original author or authors.
*
* 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 java.math.BigDecimal;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.OptionalDouble;
import java.util.OptionalInt;
import java.util.OptionalLong;
import javax.annotation.Nullable;
/**
* JdbcOperations
*
* <p>
* 定义 JdbcTemplate 的 API
* </p>
*
* @author ZhouXY108 <luquanlion@outlook.com>
* @since 1.0.0
*/
interface JdbcOperations {
// #region - query
/**
* 执行查询,并按照自定义处理逻辑对结果进行处理,将结果转换为指定类型并返回
*
* @param sql SQL
* @param params 参数
* @param resultHandler 结果处理器,用于处理 {@link ResultSet}
*/
<T> T query(String sql, Object[] params, ResultHandler<T> resultHandler)
throws SQLException;
/**
* 执行查询,并按照自定义处理逻辑对结果进行处理,将结果转换为指定类型并返回
*
* @param sql SQL
* @param resultHandler 结果处理器,用于处理 {@link ResultSet}
*/
<T> T query(String sql, ResultHandler<T> resultHandler)
throws SQLException;
// #endregion
// #region - queryList
/**
* 执行查询,将查询结果的每一行数据按照指定逻辑进行处理,返回结果列表
*
* @param sql SQL
* @param params 参数
* @param rowMapper {@link ResultSet} 中每一行的数据的处理逻辑
*/
<T> List<T> queryList(String sql, Object[] params, RowMapper<T> rowMapper)
throws SQLException;
/**
* 执行查询,返回结果映射为指定的类型。当结果为单列时使用
*
* @param sql SQL
* @param params 参数
* @param clazz 将结果映射为指定的类型
*/
<T> List<T> queryList(String sql, Object[] params, Class<T> clazz)
throws SQLException;
/**
* 执行查询,每一行数据映射为 {@code Map<String, Object>},返回结果列表
*
* @param sql SQL
* @param params 参数列表
*/
List<Map<String, Object>> queryList(String sql, Object[] params)
throws SQLException;
/**
* 执行查询,将查询结果的每一行数据按照指定逻辑进行处理,返回结果列表
*
* @param sql SQL
* @param rowMapper {@link ResultSet} 中每一行的数据的处理逻辑
*/
<T> List<T> queryList(String sql, RowMapper<T> rowMapper)
throws SQLException;
/**
* 执行查询,返回结果映射为指定的类型。当结果为单列时使用
*
* @param sql SQL
* @param clazz 将结果映射为指定的类型
*/
<T> List<T> queryList(String sql, Class<T> clazz)
throws SQLException;
/**
* 执行查询,每一行数据映射为 {@code Map<String, Object>},返回结果列表
*
* @param sql SQL
*/
List<Map<String, Object>> queryList(String sql)
throws SQLException;
// #endregion
// #region - queryFirst
/**
* 执行查询,将查询结果的第一行数据按照指定逻辑进行处理,返回 {@link Optional}
*
* @param sql SQL
* @param params 参数
* @param rowMapper {@link ResultSet} 中每一行的数据的处理逻辑
*/
<T> Optional<T> queryFirst(String sql, Object[] params, RowMapper<T> rowMapper)
throws SQLException;
/**
* 查询第一行第一列,并转换为指定类型
*
* @param <T> 目标类型
* @param sql SQL
* @param params 参数
* @param clazz 目标类型
*/
<T> Optional<T> queryFirst(String sql, Object[] params, Class<T> clazz)
throws SQLException;
/**
* 执行查询,将第一行数据转为 Map<String, Object>
*
* @param sql SQL
* @param params 参数
*/
Optional<Map<String, Object>> queryFirst(String sql, Object[] params)
throws SQLException;
/**
* 查询第一行第一列,并转换为字符串
*
* @param sql SQL
* @param params 参数
*/
Optional<String> queryFirstString(String sql, Object[] params)
throws SQLException;
/**
* 查询第一行第一列,并转换为整数值
*
* @param sql SQL
* @param params 参数
*/
OptionalInt queryFirstInt(String sql, Object[] params)
throws SQLException;
/**
* 查询第一行第一列,并转换为长整型
*
* @param sql SQL
* @param params 参数
*/
OptionalLong queryFirstLong(String sql, Object[] params)
throws SQLException;
/**
* 查询第一行第一列,并转换为双精度浮点型
*
* @param sql SQL
* @param params 参数
*/
OptionalDouble queryFirstDouble(String sql, Object[] params)
throws SQLException;
/**
* 查询第一行第一列,并转换为 {@link BigDecimal}
*
* @param sql SQL
* @param params 参数
*/
Optional<BigDecimal> queryFirstBigDecimal(String sql, Object[] params)
throws SQLException;
/**
* 执行查询,将查询结果的第一行数据按照指定逻辑进行处理,返回 {@link Optional}
*
* @param sql SQL
* @param rowMapper {@link ResultSet} 中每一行的数据的处理逻辑
*/
<T> Optional<T> queryFirst(String sql, RowMapper<T> rowMapper)
throws SQLException;
/**
* 查询第一行第一列,并转换为指定类型
*
* @param <T> 目标类型
* @param sql SQL
* @param clazz 目标类型
*/
<T> Optional<T> queryFirst(String sql, Class<T> clazz)
throws SQLException;
/**
* 执行查询,将第一行数据转为 Map<String, Object>
*
* @param sql SQL
*/
Optional<Map<String, Object>> queryFirst(String sql)
throws SQLException;
/**
* 查询第一行第一列,并转换为字符串
*
* @param sql SQL
*/
Optional<String> queryFirstString(String sql)
throws SQLException;
/**
* 查询第一行第一列,并转换为整数值
*
* @param sql SQL
*/
OptionalInt queryFirstInt(String sql)
throws SQLException;
/**
* 查询第一行第一列,并转换为长整型
*
* @param sql SQL
*/
OptionalLong queryFirstLong(String sql)
throws SQLException;
/**
* 查询第一行第一列,并转换为双精度浮点型
*
* @param sql SQL
*/
OptionalDouble queryFirstDouble(String sql)
throws SQLException;
/**
* 查询第一行第一列,并转换为 {@link BigDecimal}
*
* @param sql SQL
*/
Optional<BigDecimal> queryFirstBigDecimal(String sql)
throws SQLException;
/**
* 查询结果,并转换为 boolean
*
* @param sql SQL
*/
boolean queryAsBoolean(String sql)
throws SQLException;
/**
* 查询结果,并转换为 boolean
*
* @param sql SQL
*/
boolean queryAsBoolean(String sql, Object[] params)
throws SQLException;
// #endregion
// #region - update & batchUpdate
/**
* 执行更新操作
*
* @param sql 要执行的 SQL
* @param params 参数
* @return 更新记录数
*/
int update(String sql, Object[] params)
throws SQLException;
/**
* 执行更新操作
*
* @param sql 要执行的 SQL
* @return 更新记录数
*/
int update(String sql)
throws SQLException;
/**
* 执行 SQL 并返回生成的 keys
*
* @param sql 要执行的 SQL
* @param params 参数
* @param rowMapper 行数据映射逻辑
*
* @return generated keys
* @throws SQLException 执行 SQL 遇到异常情况将抛出
*/
<T> List<T> update(String sql, Object[] params, RowMapper<T> rowMapper)
throws SQLException;
/**
* 执行 SQL 并返回生成的 keys
*
* @param sql 要执行的 SQL
* @param rowMapper 行数据映射逻辑
*
* @return generated keys
* @throws SQLException 执行 SQL 遇到异常情况将抛出
*/
<T> List<T> update(String sql, RowMapper<T> rowMapper)
throws SQLException;
/**
* 执行批量更新,批量更新数据,返回每条记录更新的行数
*
* @param sql SQL 语句
* @param params 参数列表
* @param batchSize 每次批量更新的数据量
*/
List<int[]> batchUpdate(String sql, @Nullable Collection<Object[]> params, int batchSize)
throws SQLException;
/**
* 批量更新,返回更新成功的记录行数。发生异常时不中断操作,将异常存入 {@code exceptions} 中
*
* @param sql sql语句
* @param params 参数列表
* @param batchSize 每次批量更新的数据量
* @param exceptions 异常列表,用于记录异常信息
*/
List<int[]> batchUpdateAndIgnoreException(String sql, @Nullable Collection<Object[]> params,
int batchSize, List<Exception> exceptions)
throws SQLException;
// #endregion
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2022-2024 the original author or authors.
* Copyright 2022-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -16,6 +16,7 @@
package xyz.zhouxy.jdbc;
import java.sql.PreparedStatement;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
@@ -26,33 +27,54 @@ import java.util.OptionalInt;
import java.util.OptionalLong;
import java.util.function.Function;
import java.util.stream.Collectors;
import com.google.common.base.Preconditions;
import java.util.stream.Stream;
import xyz.zhouxy.plusone.commons.collection.CollectionTools;
import xyz.zhouxy.plusone.commons.util.ArrayTools;
import xyz.zhouxy.plusone.commons.util.AssertTools;
import xyz.zhouxy.plusone.commons.util.OptionalTools;
/**
* ParamBuilder
*
* <p>
* JDBC 参数构造器,将数据转换为 {@code Object[]} 类型,以传给 {@link PreparedStatement}
* </p>
*
* @author ZhouXY108 <luquanlion@outlook.com>
* @since 1.0.0
*/
public class ParamBuilder {
public static final Object[] EMPTY_OBJECT_ARRAY = {};
public static Object[] buildParams(final Object... params) {
if (ArrayTools.isNullOrEmpty(params)) {
if (ArrayTools.isEmpty(params)) {
return EMPTY_OBJECT_ARRAY;
}
return Arrays.stream(params)
return buildParamsFromStream(Arrays.stream(params));
}
public static Object[] buildParams(final Collection<?> params) {
if (CollectionTools.isEmpty(params)) {
return EMPTY_OBJECT_ARRAY;
}
return buildParamsFromStream(params.stream());
}
private static Object[] buildParamsFromStream(Stream<?> stream) {
return stream
.map(param -> {
if (param instanceof Optional) {
return OptionalTools.orElseNull((Optional<?>) param);
}
if (param instanceof OptionalInt) {
return OptionalTools.toInteger(((OptionalInt) param));
return OptionalTools.toInteger((OptionalInt) param);
}
if (param instanceof OptionalLong) {
return OptionalTools.toLong(((OptionalLong) param));
return OptionalTools.toLong((OptionalLong) param);
}
if (param instanceof OptionalDouble) {
return OptionalTools.toDouble(((OptionalDouble) param));
return OptionalTools.toDouble((OptionalDouble) param);
}
return param;
})
@@ -60,8 +82,8 @@ public class ParamBuilder {
}
public static <T> List<Object[]> buildBatchParams(final Collection<T> c, final Function<T, Object[]> func) {
Preconditions.checkNotNull(c, "The collection can not be null.");
Preconditions.checkNotNull(func, "The func can not be null.");
AssertTools.checkNotNull(c, "The collection can not be null.");
AssertTools.checkNotNull(func, "The func can not be null.");
if (CollectionTools.isEmpty(c)) {
return Collections.emptyList();
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2024 the original author or authors.
* Copyright 2024-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -19,7 +19,21 @@ package xyz.zhouxy.jdbc;
import java.sql.ResultSet;
import java.sql.SQLException;
/**
* ResultHandler
*
* <p>
* 处理 {@link ResultSet}
* </p>
*
* @author ZhouXY108 <luquanlion@outlook.com>
* @since 1.0.0
*/
@FunctionalInterface
public interface ResultHandler<T> {
/**
* 将 {@link ResultSet} 转换为指定类型的对象
*/
T handle(ResultSet resultSet) throws SQLException;
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2022-2024 the original author or authors.
* Copyright 2022-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -22,11 +22,22 @@ import java.sql.SQLException;
import java.util.HashMap;
import java.util.Map;
/**
* RowMapper
*
* <p>
* {@link ResultSet} 中每一行数据的处理逻辑。
* </p>
*
* @author ZhouXY108 <luquanlion@outlook.com>
* @since 1.0.0
*/
@FunctionalInterface
public interface RowMapper<T> {
T mapRow(ResultSet rs, int rowNumber) throws SQLException;
public static final RowMapper<Map<String, Object>> HASH_MAP_MAPPER = (rs, rowNumber) -> {
/** 每一行数据转换为 {@link HashMap} */
RowMapper<Map<String, Object>> HASH_MAP_MAPPER = (rs, rowNumber) -> {
Map<String, Object> result = new HashMap<>();
ResultSetMetaData metaData = rs.getMetaData();
int columnCount = metaData.getColumnCount();
@@ -37,14 +48,13 @@ public interface RowMapper<T> {
return result;
};
public static final RowMapper<DbRecord> RECORD_MAPPER =
(rs, rowNumber) -> new DbRecord(HASH_MAP_MAPPER.mapRow(rs, rowNumber));
public static <T> RowMapper<T> beanRowMapper(Class<T> beanType) throws SQLException {
/** 默认实现的将 {@link ResultSet} 转换为 Java Bean 的 {@link RowMapper}。 */
static <T> RowMapper<T> beanRowMapper(Class<T> beanType) throws SQLException {
return DefaultBeanRowMapper.of(beanType);
}
public static <T> RowMapper<T> beanRowMapper(Class<T> beanType, Map<String, String> propertyColMap)
/** 默认实现的将 {@link ResultSet} 转换为 Java Bean 的 {@link RowMapper}。 */
static <T> RowMapper<T> beanRowMapper(Class<T> beanType, Map<String, String> propertyColMap)
throws SQLException {
return DefaultBeanRowMapper.of(beanType, propertyColMap);
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,137 @@
/*
* Copyright 2025 the original author or authors.
*
* 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.test;
import java.time.LocalDateTime;
import java.util.Objects;
public class AccountPO {
Long id;
String username;
String accountStatus;
LocalDateTime createTime;
Long createdBy;
LocalDateTime updateTime;
Long updatedBy;
Long version;
public AccountPO() {
}
public AccountPO(Long id, String username, String accountStatus, LocalDateTime createTime, Long createdBy,
LocalDateTime updateTime, Long updatedBy, Long version) {
this.id = id;
this.username = username;
this.accountStatus = accountStatus;
this.createTime = createTime;
this.createdBy = createdBy;
this.updateTime = updateTime;
this.updatedBy = updatedBy;
this.version = version;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getAccountStatus() {
return accountStatus;
}
public void setAccountStatus(String accountStatus) {
this.accountStatus = accountStatus;
}
public LocalDateTime getCreateTime() {
return createTime;
}
public void setCreateTime(LocalDateTime createTime) {
this.createTime = createTime;
}
public Long getCreatedBy() {
return createdBy;
}
public void setCreatedBy(Long createdBy) {
this.createdBy = createdBy;
}
public LocalDateTime getUpdateTime() {
return updateTime;
}
public void setUpdateTime(LocalDateTime updateTime) {
this.updateTime = updateTime;
}
public Long getUpdatedBy() {
return updatedBy;
}
public void setUpdatedBy(Long updatedBy) {
this.updatedBy = updatedBy;
}
public Long getVersion() {
return version;
}
public void setVersion(Long version) {
this.version = version;
}
@Override
public int hashCode() {
return Objects.hash(id, username, accountStatus, createTime, createdBy, updateTime, updatedBy, version);
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
AccountPO other = (AccountPO) obj;
return Objects.equals(id, other.id) && Objects.equals(username, other.username)
&& Objects.equals(accountStatus, other.accountStatus) && Objects.equals(createTime, other.createTime)
&& Objects.equals(createdBy, other.createdBy) && Objects.equals(updateTime, other.updateTime)
&& Objects.equals(updatedBy, other.updatedBy) && Objects.equals(version, other.version);
}
@Override
public String toString() {
return "AccountPO [id=" + id + ", username=" + username + ", accountStatus=" + accountStatus + ", createTime="
+ createTime + ", createdBy=" + createdBy + ", updateTime=" + updateTime + ", updatedBy=" + updatedBy
+ ", version=" + version + "]";
}
}

View File

@@ -1,32 +1,47 @@
/*
* Copyright 2023-2025 the original author or authors.
*
* 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.test;
import static org.junit.jupiter.api.Assertions.*;
import static xyz.zhouxy.jdbc.ParamBuilder.*;
import static xyz.zhouxy.plusone.commons.sql.JdbcSql.*;
import static xyz.zhouxy.jdbc.ParamBuilder.buildParams;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.sql.SQLException;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import org.h2.jdbcx.JdbcDataSource;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Lists;
import com.google.common.io.Resources;
import xyz.zhouxy.jdbc.DbRecord;
import xyz.zhouxy.jdbc.RowMapper;
import xyz.zhouxy.jdbc.SimpleJdbcTemplate;
import xyz.zhouxy.jdbc.SimpleJdbcTemplate.JdbcExecutor;
import xyz.zhouxy.plusone.commons.sql.SQL;
import xyz.zhouxy.plusone.commons.util.IdGenerator;
import xyz.zhouxy.plusone.commons.util.IdWorker;
@@ -34,78 +49,61 @@ class SimpleJdbcTemplateTests {
private static final Logger log = LoggerFactory.getLogger(SimpleJdbcTemplateTests.class);
private static final SimpleJdbcTemplate jdbcTemplate;
private static SimpleJdbcTemplate jdbcTemplate;
static {
@BeforeAll
static void initH2() throws IOException, SQLException {
JdbcDataSource dataSource = new JdbcDataSource();
dataSource.setURL("jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1;DATABASE_TO_UPPER=FALSE;MODE=MySQL");
dataSource.setUser("sa");
dataSource.setPassword("");
jdbcTemplate = new SimpleJdbcTemplate(dataSource);
// 建表
executeSqlFile("schema.sql");
}
@BeforeAll
static void setUp() throws SQLException {
jdbcTemplate.update("CREATE TABLE sys_account ("
+ "\n" + " id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY"
+ "\n" + " ,username VARCHAR(255) NOT NULL"
+ "\n" + " ,account_status VARCHAR(2) NOT NULL"
+ "\n" + " ,create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP"
+ "\n" + " ,created_by BIGINT NOT NULL"
+ "\n" + " ,update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP"
+ "\n" + " ,updated_by BIGINT DEFAULT NULL"
+ "\n" + " ,version BIGINT NOT NULL DEFAULT 0"
+ "\n" + ")");
jdbcTemplate.batchUpdate("INSERT INTO sys_account(id, username, account_status, created_by) VALUES (?, ?, ?, ?)", Lists.newArrayList(
buildParams(2L, "zhouxy2", "0", 108L),
buildParams(3L, "zhouxy3", "0", 108L),
buildParams(4L, "zhouxy4", "0", 108L),
buildParams(5L, "zhouxy5", "0", 108L),
buildParams(6L, "zhouxy6", "0", 108L),
buildParams(7L, "zhouxy7", "0", 108L),
buildParams(8L, "zhouxy8", "0", 108L),
buildParams(9L, "zhouxy9", "0", 108L)
), 10);
jdbcTemplate.batchUpdate("INSERT INTO sys_account(id, username, account_status, created_by, create_time, update_time, version) VALUES (?, ?, ?, ?, ?, ?, ?)", Lists.newArrayList(
buildParams(10L, "zhouxy10", "1", 118L, LocalDate.of(2000, 1, 1), LocalDate.of(2000, 1, 29), 31),
buildParams(11L, "zhouxy11", "1", 118L, LocalDate.of(2000, 1, 1), LocalDate.of(2000, 1, 29), 28),
buildParams(12L, "zhouxy12", "1", 118L, LocalDate.of(2000, 1, 1), LocalDate.of(2000, 1, 29), 25),
buildParams(13L, "zhouxy13", "1", 118L, LocalDate.of(2000, 1, 1), LocalDate.of(2000, 1, 29), 22),
buildParams(14L, "zhouxy14", "1", 118L, LocalDate.of(2000, 1, 1), LocalDate.of(2000, 1, 29), 19),
buildParams(15L, "zhouxy15", "1", 118L, LocalDate.of(2000, 1, 1), LocalDate.of(2000, 1, 29), 16),
buildParams(16L, "zhouxy16", "1", 118L, LocalDate.of(2000, 1, 1), LocalDate.of(2000, 1, 29), 13),
buildParams(17L, "zhouxy17", "1", 118L, LocalDate.of(2000, 1, 1), LocalDate.of(2000, 1, 29), 10),
buildParams(18L, "zhouxy18", "1", 118L, LocalDate.of(2000, 1, 1), LocalDate.of(2000, 1, 29), 7),
buildParams(19L, "zhouxy19", "1", 118L, LocalDate.of(2000, 1, 1), LocalDate.of(2000, 1, 29), 0)
), 10);
jdbcTemplate.update("INSERT INTO sys_account(id, username, account_status, created_by, create_time, updated_by, update_time, version) VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
buildParams(20L, "zhouxy20", "2", 118L, LocalDateTime.of(2008, 8, 8, 20, 8), 31L, LocalDateTime.now(), 88L));
@BeforeEach
void initData() throws IOException, SQLException {
// 初始化数据
executeSqlFile("data.sql");
}
static void executeSqlFile(String filePath) throws IOException, SQLException {
String[] sqls = Resources
.toString(Resources.getResource(filePath), StandardCharsets.UTF_8)
.split(";");
for (String sql : sqls) {
jdbcTemplate.update(sql);
}
}
@Test
void testQuery() throws SQLException {
Object[] ids = buildParams(5, 9, 13, 14, 17, 20, 108);
String sql = SQL.newJdbcSql()
.SELECT("id", "username", "account_status")
.FROM("sys_account")
.WHERE(IN("id", ids))
.toString();
String sql = "SELECT id, username, account_status FROM sys_account WHERE id IN (?, ?, ?, ?, ?, ?, ?)";
log.info(sql);
List<DbRecord> rs = jdbcTemplate.queryRecordList(sql, ids);
for (DbRecord dbRecord : rs) {
List<Map<String, Object>> rs = jdbcTemplate.queryList(sql, ids);
for (Map<String, Object> dbRecord : rs) {
log.info("{}", dbRecord);
}
assertEquals(
Lists.newArrayList(
new DbRecord(ImmutableMap.of("id", 5L, "account_status", "0", "username", "zhouxy5")),
new DbRecord(ImmutableMap.of("id", 9L, "account_status", "0", "username", "zhouxy9")),
new DbRecord(ImmutableMap.of("id", 13L, "account_status", "1", "username", "zhouxy13")),
new DbRecord(ImmutableMap.of("id", 14L, "account_status", "1", "username", "zhouxy14")),
new DbRecord(ImmutableMap.of("id", 17L, "account_status", "1", "username", "zhouxy17")),
new DbRecord(ImmutableMap.of("id", 20L, "account_status", "2", "username", "zhouxy20"))
),
rs
List<ImmutableMap<String, Object>> expected = ImmutableList.of(
ImmutableMap.of("id", 5L, "account_status", "0", "username", "zhouxy5"),
ImmutableMap.of("id", 9L, "account_status", "0", "username", "zhouxy9"),
ImmutableMap.of("id", 13L, "account_status", "1", "username", "zhouxy13"),
ImmutableMap.of("id", 14L, "account_status", "1", "username", "zhouxy14"),
ImmutableMap.of("id", 17L, "account_status", "1", "username", "zhouxy17"),
ImmutableMap.of("id", 20L, "account_status", "2", "username", "zhouxy20")
);
assertEquals(expected, rs);
}
@Test
void testQueryExists() throws SQLException {
boolean isExists = jdbcTemplate.queryAsBoolean(
"SELECT EXISTS(SELECT 1 FROM sys_account WHERE id = ? LIMIT 1)",
buildParams(998));
assertFalse(isExists);
}
@Test
@@ -132,16 +130,16 @@ class SimpleJdbcTemplateTests {
@Test
void testUpdate() throws SQLException {
List<DbRecord> keys = jdbcTemplate.update(
List<Map<String, Object>> keys = jdbcTemplate.update(
"UPDATE sys_account SET account_status = ?, version = version + 1, update_time = now(), updated_by = ? WHERE id = ? AND version = ?",
buildParams("7", 886L, 20L, 88L),
RowMapper.RECORD_MAPPER);
RowMapper.HASH_MAP_MAPPER);
assertEquals(1, keys.size());
log.info("keys: {}", keys);
keys = jdbcTemplate.update(
"UPDATE sys_account SET account_status = ?, version = version + 1, update_time = now(), updated_by = ? WHERE id = ? AND version = ?",
buildParams("-1", 886L, 20L, 88L),
RowMapper.RECORD_MAPPER);
RowMapper.HASH_MAP_MAPPER);
assertEquals(0, keys.size());
}
@@ -165,7 +163,7 @@ class SimpleJdbcTemplateTests {
Optional<Map<String, Object>> first = jdbcTemplate
.queryFirst("SELECT * FROM sys_account WHERE id = ?", buildParams(id));
log.info("first: {}", first);
assertTrue(!first.isPresent());
assertFalse(first.isPresent());
}
// 没有异常,提交事务
@@ -198,7 +196,7 @@ class SimpleJdbcTemplateTests {
Optional<Map<String, Object>> first = jdbcTemplate
.queryFirst("SELECT * FROM sys_account WHERE id = ?", buildParams(id));
log.info("first: {}", first);
assertTrue(!first.isPresent());
assertFalse(first.isPresent());
}
// 返回 false回滚
@@ -213,7 +211,7 @@ class SimpleJdbcTemplateTests {
Optional<Map<String, Object>> first = jdbcTemplate
.queryFirst("SELECT * FROM sys_account WHERE id = ?", buildParams(id));
log.info("first: {}", first);
assertTrue(!first.isPresent());
assertFalse(first.isPresent());
}
// 返回 true提交事务
@@ -246,120 +244,3 @@ class SimpleJdbcTemplateTests {
log.info("{}", t);
}
}
class AccountPO {
Long id;
String username;
String accountStatus;
LocalDateTime createTime;
Long createdBy;
LocalDateTime updateTime;
Long updatedBy;
Long version;
public AccountPO() {
}
public AccountPO(Long id, String username, String accountStatus, LocalDateTime createTime, Long createdBy,
LocalDateTime updateTime, Long updatedBy, Long version) {
this.id = id;
this.username = username;
this.accountStatus = accountStatus;
this.createTime = createTime;
this.createdBy = createdBy;
this.updateTime = updateTime;
this.updatedBy = updatedBy;
this.version = version;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getAccountStatus() {
return accountStatus;
}
public void setAccountStatus(String accountStatus) {
this.accountStatus = accountStatus;
}
public LocalDateTime getCreateTime() {
return createTime;
}
public void setCreateTime(LocalDateTime createTime) {
this.createTime = createTime;
}
public Long getCreatedBy() {
return createdBy;
}
public void setCreatedBy(Long createdBy) {
this.createdBy = createdBy;
}
public LocalDateTime getUpdateTime() {
return updateTime;
}
public void setUpdateTime(LocalDateTime updateTime) {
this.updateTime = updateTime;
}
public Long getUpdatedBy() {
return updatedBy;
}
public void setUpdatedBy(Long updatedBy) {
this.updatedBy = updatedBy;
}
public Long getVersion() {
return version;
}
public void setVersion(Long version) {
this.version = version;
}
@Override
public int hashCode() {
return Objects.hash(id, username, accountStatus, createTime, createdBy, updateTime, updatedBy, version);
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
AccountPO other = (AccountPO) obj;
return Objects.equals(id, other.id) && Objects.equals(username, other.username)
&& Objects.equals(accountStatus, other.accountStatus) && Objects.equals(createTime, other.createTime)
&& Objects.equals(createdBy, other.createdBy) && Objects.equals(updateTime, other.updateTime)
&& Objects.equals(updatedBy, other.updatedBy) && Objects.equals(version, other.version);
}
@Override
public String toString() {
return "AccountPO [id=" + id + ", username=" + username + ", accountStatus=" + accountStatus + ", createTime="
+ createTime + ", createdBy=" + createdBy + ", updateTime=" + updateTime + ", updatedBy=" + updatedBy
+ ", version=" + version + "]";
}
}

View File

@@ -0,0 +1,23 @@
truncate table sys_account;
INSERT INTO sys_account(id, username, account_status, created_by) VALUES (2, 'zhouxy2', '0', 108);
INSERT INTO sys_account(id, username, account_status, created_by) VALUES (3, 'zhouxy3', '0', 108);
INSERT INTO sys_account(id, username, account_status, created_by) VALUES (4, 'zhouxy4', '0', 108);
INSERT INTO sys_account(id, username, account_status, created_by) VALUES (5, 'zhouxy5', '0', 108);
INSERT INTO sys_account(id, username, account_status, created_by) VALUES (6, 'zhouxy6', '0', 108);
INSERT INTO sys_account(id, username, account_status, created_by) VALUES (7, 'zhouxy7', '0', 108);
INSERT INTO sys_account(id, username, account_status, created_by) VALUES (8, 'zhouxy8', '0', 108);
INSERT INTO sys_account(id, username, account_status, created_by) VALUES (9, 'zhouxy9', '0', 108);
INSERT INTO sys_account(id, username, account_status, created_by, create_time, update_time, `version`) VALUES (10, 'zhouxy10', '1', 118, '2000-01-01', '2000-01-29', 31);
INSERT INTO sys_account(id, username, account_status, created_by, create_time, update_time, `version`) VALUES (11, 'zhouxy11', '1', 118, '2000-01-01', '2000-01-29', 28);
INSERT INTO sys_account(id, username, account_status, created_by, create_time, update_time, `version`) VALUES (12, 'zhouxy12', '1', 118, '2000-01-01', '2000-01-29', 25);
INSERT INTO sys_account(id, username, account_status, created_by, create_time, update_time, `version`) VALUES (13, 'zhouxy13', '1', 118, '2000-01-01', '2000-01-29', 22);
INSERT INTO sys_account(id, username, account_status, created_by, create_time, update_time, `version`) VALUES (14, 'zhouxy14', '1', 118, '2000-01-01', '2000-01-29', 19);
INSERT INTO sys_account(id, username, account_status, created_by, create_time, update_time, `version`) VALUES (15, 'zhouxy15', '1', 118, '2000-01-01', '2000-01-29', 16);
INSERT INTO sys_account(id, username, account_status, created_by, create_time, update_time, `version`) VALUES (16, 'zhouxy16', '1', 118, '2000-01-01', '2000-01-29', 13);
INSERT INTO sys_account(id, username, account_status, created_by, create_time, update_time, `version`) VALUES (17, 'zhouxy17', '1', 118, '2000-01-01', '2000-01-29', 10);
INSERT INTO sys_account(id, username, account_status, created_by, create_time, update_time, `version`) VALUES (18, 'zhouxy18', '1', 118, '2000-01-01', '2000-01-29', 7 );
INSERT INTO sys_account(id, username, account_status, created_by, create_time, update_time, `version`) VALUES (19, 'zhouxy19', '1', 118, '2000-01-01', '2000-01-29', 0 );
INSERT INTO sys_account(id, username, account_status, created_by, create_time, updated_by, update_time, version) VALUES (20, 'zhouxy20', '2', 118, '2008-08-08 20:08:00', 31, now(), 88);

View File

@@ -0,0 +1,10 @@
CREATE TABLE sys_account (
`id` BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY
, `username` VARCHAR(255) NOT NULL
, `account_status` VARCHAR(2) NOT NULL
, `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
, `created_by` BIGINT NOT NULL
, `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
, `updated_by` BIGINT DEFAULT NULL
, `version` BIGINT NOT NULL DEFAULT 0
)