Compare commits

..

36 Commits

Author SHA1 Message Date
52f82d69e8 chore: add lincese header 2025-07-29 17:35:24 +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
bd08be5928 添加 .editorconfig 文件 2024-11-02 02:09:53 +08:00
a1b6492355 测试代码中演示如何获取 id。 2024-11-02 00:22:26 +08:00
f5cef8f730 改为使用 H2 进行单元测试。 2024-11-02 00:14:30 +08:00
023bd15f06 修改版本号 2024-11-01 21:43:00 +08:00
970435ed1f 更新 plusone-commons 版本 2024-11-01 21:35:51 +08:00
e4e94d5686 修改版本号 2024-11-01 17:15:37 +08:00
9e035d1b53 重构部分代码;修改方法名。 2024-11-01 16:46:17 +08:00
2ab0492e8c 新增不传 sql 参数的重载方法 2024-10-21 20:56:38 +08:00
c6582ecf6b 优化代码,默认属性名为小驼峰命名,数据库列名为下划线命名。 2024-10-03 17:12:32 +08:00
3ad9c9fddb 添加许可证。 2024-10-03 16:32:34 +08:00
7f31d477d6 新增 DefaultBeanResultMap 2024-10-03 16:27:19 +08:00
fda69cea6b 重构。Connection 不通过参数传进来,避免用户在传进 Connection 前做不可预料的操作。 2024-10-03 10:23:17 +08:00
3ca9ad2be1 添加 commitIfTrue。 2024-10-03 08:46:24 +08:00
354fe42e0d 添加 README.md 2024-08-01 10:50:15 +08:00
0fea97f2a1 添加方法,允许执行 SQL 后获取更新后的数据。 2024-08-01 10:10:59 +08:00
23 changed files with 2678 additions and 468 deletions

18
.editorconfig Normal file
View File

@@ -0,0 +1,18 @@
# EditorConfig is awesome: https://EditorConfig.org
# top-most EditorConfig file
root = true
[*]
indent_style = space
indent_size = 4
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>

202
LICENSE.txt Normal file
View File

@@ -0,0 +1,202 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
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
http://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.

181
README.md Normal file
View File

@@ -0,0 +1,181 @@
# SimpleJDBC
对 JDBC 的简单封装。
之前遇到的一个老项目,没有引入任何 ORM 框架,对数据库的操作几乎都在写原生 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 的学习参考。**

50
pom.xml
View File

@@ -6,57 +6,55 @@
<groupId>xyz.zhouxy.jdbc</groupId>
<artifactId>simple-jdbc</artifactId>
<version>0.1.0-SNAPSHOT</version>
<version>1.0.0-alpha</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-RC1</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>0.1.0-SNAPSHOT</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>
<!-- https://mvnrepository.com/artifact/com.h2database/h2 -->
<dependency>
<groupId>com.zaxxer</groupId>
<artifactId>HikariCP</artifactId>
<version>4.0.3</version>
<exclusions>
<exclusion>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
</exclusion>
</exclusions>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<version>42.3.8</version>
<exclusions>
<exclusion>
<groupId>org.checkerframework</groupId>
<artifactId>checker-qual</artifactId>
</exclusion>
</exclusions>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

View File

@@ -1,77 +0,0 @@
/*
* Copyright 2022-2023 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 org.apache.commons.lang3.StringUtils;
import xyz.zhouxy.plusone.commons.collection.AbstractMapWrapper;
import xyz.zhouxy.plusone.commons.util.OptionalTools;
import java.util.*;
@Beta
public class DbRecord extends AbstractMapWrapper<String, Object, DbRecord> {
public DbRecord() {
super(new HashMap<>(), k -> Preconditions.checkArgument(StringUtils.isNotBlank(k), "Key must has text."), null);
}
public DbRecord(Map<String, Object> map) {
super(map, k -> Preconditions.checkArgument(StringUtils.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

@@ -0,0 +1,167 @@
/*
* 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.
* 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.beans.BeanInfo;
import java.beans.IntrospectionException;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.util.Arrays;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;
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) {
this.constructor = constructor;
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 {
// 获取无参构造器
Constructor<T> constructor = beanType.getDeclaredConstructor();
constructor.setAccessible(true); // NOSONAR
final Map<String, PropertyDescriptor> colPropertyMap = buildColPropertyMap(beanType, propertyColMap);
return new DefaultBeanRowMapper<>(constructor, colPropertyMap);
}
catch (IntrospectionException e) {
throw new SQLException("There is an exception occurs during introspection.", e);
}
catch (NoSuchMethodException e) {
throw new SQLException("Could not find a no-args constructor in " + beanType.getName(), e);
}
}
/** {@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();
if (setter != null) {
Class<?> propertyType = propertyDescriptor.getPropertyType();
setter.setAccessible(true); // NOSONAR
setter.invoke(newInstance, rs.getObject(colName, propertyType));
}
}
}
return newInstance;
}
catch (IllegalAccessException | InstantiationException | InvocationTargetException e) {
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

@@ -0,0 +1,96 @@
/*
* 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.
* 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.sql.PreparedStatement;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.OptionalDouble;
import java.util.OptionalInt;
import java.util.OptionalLong;
import java.util.function.Function;
import java.util.stream.Collectors;
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.isEmpty(params)) {
return EMPTY_OBJECT_ARRAY;
}
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);
}
if (param instanceof OptionalLong) {
return OptionalTools.toLong((OptionalLong) param);
}
if (param instanceof OptionalDouble) {
return OptionalTools.toDouble((OptionalDouble) param);
}
return param;
})
.toArray();
}
public static <T> List<Object[]> buildBatchParams(final Collection<T> c, final Function<T, Object[]> func) {
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();
}
return c.stream().map(func).collect(Collectors.toList());
}
private ParamBuilder() {
throw new IllegalStateException("Utility class");
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2022-2023 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.
@@ -16,13 +16,24 @@
package xyz.zhouxy.jdbc;
import com.google.common.annotations.Beta;
import java.sql.ResultSet;
import java.sql.SQLException;
@Beta
/**
* ResultHandler
*
* <p>
* 处理 {@link ResultSet}
* </p>
*
* @author ZhouXY108 <luquanlion@outlook.com>
* @since 1.0.0
*/
@FunctionalInterface
public interface ResultMap<T> {
T map(ResultSet rs, int rowNumber) throws SQLException;
public interface ResultHandler<T> {
/**
* {@link ResultSet} 转换为指定类型的对象
*/
T handle(ResultSet resultSet) throws SQLException;
}

View File

@@ -0,0 +1,61 @@
/*
* 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.
* 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.sql.ResultSet;
import java.sql.ResultSetMetaData;
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;
/** 每一行数据转换为 {@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();
for (int i = 1; i <= columnCount; i++) {
String colName = metaData.getColumnName(i);
result.put(colName, rs.getObject(colName));
}
return result;
};
/** 默认实现的将 {@link ResultSet} 转换为 Java Bean 的 {@link RowMapper}。 */
static <T> RowMapper<T> beanRowMapper(Class<T> beanType) throws SQLException {
return DefaultBeanRowMapper.of(beanType);
}
/** 默认实现的将 {@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);
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2022-2023 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.
@@ -18,269 +18,734 @@ package xyz.zhouxy.jdbc;
import java.math.BigDecimal;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
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 java.util.function.Function;
import java.util.stream.Collectors;
import org.apache.commons.lang3.ArrayUtils;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.sql.DataSource;
import com.google.common.annotations.Beta;
import com.google.common.base.Preconditions;
import com.google.common.collect.Lists;
import xyz.zhouxy.plusone.commons.collection.CollectionTools;
import xyz.zhouxy.plusone.commons.util.ArrayTools;
import xyz.zhouxy.plusone.commons.function.ThrowingConsumer;
import xyz.zhouxy.plusone.commons.function.ThrowingPredicate;
import xyz.zhouxy.plusone.commons.util.AssertTools;
import xyz.zhouxy.plusone.commons.util.OptionalTools;
@Beta
public class SimpleJdbcTemplate {
/**
* SimpleJdbcTemplate
*
* <p>
* 对 JDBC 的简单封装,方便数据库操作,支持事务,支持批量操作,支持自定义结果集映射
* </p>
*
* @author ZhouXY108 <luquanlion@outlook.com>
* @since 1.0.0
*/
public class SimpleJdbcTemplate implements JdbcOperations {
public static JdbcExecutor connect(final Connection conn) {
return new JdbcExecutor(conn);
@Nonnull
private final DataSource dataSource;
public SimpleJdbcTemplate(@Nonnull DataSource dataSource) {
AssertTools.checkNotNull(dataSource);
this.dataSource = dataSource;
}
public static String paramsToString(Object[] params) {
return Arrays.toString(params);
// #region - query
/** {@inheritDoc} */
@Override
public <T> T query(String sql, Object[] params, ResultHandler<T> resultHandler)
throws SQLException {
try (Connection conn = this.dataSource.getConnection()) {
return JdbcOperationSupport.query(conn, sql, params, resultHandler);
}
}
public static String paramsToString(final Collection<Object[]> params) {
if (params == null) {
return "null";
/** {@inheritDoc} */
@Override
public <T> T query(String sql, ResultHandler<T> resultHandler)
throws SQLException {
try (Connection conn = this.dataSource.getConnection()) {
return JdbcOperationSupport.query(conn, sql, ParamBuilder.EMPTY_OBJECT_ARRAY, resultHandler);
}
if (params.isEmpty()) {
return "[]";
}
int iMax = params.size() - 1;
StringBuilder b = new StringBuilder();
b.append('[');
int i = 0;
for (Object[] p : params) {
b.append(Arrays.toString(p));
if (i == iMax) {
return b.append(']').toString();
}
b.append(',');
i++;
}
return b.append(']').toString();
}
private SimpleJdbcTemplate() {
throw new IllegalStateException("Utility class");
// #endregion
// #region - queryList
/** {@inheritDoc} */
@Override
public <T> List<T> queryList(String sql, Object[] params, RowMapper<T> rowMapper)
throws SQLException {
try (Connection conn = this.dataSource.getConnection()) {
return JdbcOperationSupport.queryList(conn, sql, params, rowMapper);
}
}
public static class JdbcExecutor {
private final Connection conn;
public JdbcExecutor(Connection conn) {
this.conn = conn;
/** {@inheritDoc} */
@Override
public <T> List<T> queryList(String sql, Object[] params, Class<T> clazz)
throws SQLException {
try (Connection conn = this.dataSource.getConnection()) {
return JdbcOperationSupport.queryList(conn, sql, params, clazz);
}
}
public <T> List<T> query(String sql, Object[] params, ResultMap<T> resultMap) throws SQLException {
try (PreparedStatement stmt = this.conn.prepareStatement(sql)) {
fillStatement(stmt, params);
try (ResultSet rs = stmt.executeQuery()) {
List<T> result = new ArrayList<>();
int rowNumber = 0;
while (rs.next()) {
T e = resultMap.map(rs, rowNumber++);
result.add(e);
}
return result;
}
}
/** {@inheritDoc} */
@Override
public List<Map<String, Object>> queryList(String sql, Object[] params)
throws SQLException {
try (Connection conn = this.dataSource.getConnection()) {
return JdbcOperationSupport.queryList(conn, sql, params, RowMapper.HASH_MAP_MAPPER);
}
}
public <T> Optional<T> queryFirst(String sql, Object[] params, ResultMap<T> resultMap) throws SQLException {
return query(sql, params, resultMap).stream().findFirst();
/** {@inheritDoc} */
@Override
public <T> List<T> queryList(String sql, RowMapper<T> rowMapper)
throws SQLException {
try (Connection conn = this.dataSource.getConnection()) {
return JdbcOperationSupport.queryList(conn, sql, ParamBuilder.EMPTY_OBJECT_ARRAY, rowMapper);
}
}
public static final ResultMap<Map<String, Object>> mapResultMap = (rs, rowNumber) -> {
Map<String, Object> result = new HashMap<>();
ResultSetMetaData metaData = rs.getMetaData();
int columnCount = metaData.getColumnCount();
for (int i = 1; i <= columnCount; i++) {
String colName = metaData.getColumnName(i);
result.put(colName, rs.getObject(colName));
}
return result;
};
public List<Map<String, Object>> query(String sql, Object... params) throws SQLException {
return query(sql, params, mapResultMap);
/** {@inheritDoc} */
@Override
public <T> List<T> queryList(String sql, Class<T> clazz)
throws SQLException {
try (Connection conn = this.dataSource.getConnection()) {
return JdbcOperationSupport.queryList(conn, sql, ParamBuilder.EMPTY_OBJECT_ARRAY, clazz);
}
}
public Optional<Map<String, Object>> queryFirst(String sql, Object... params) throws SQLException {
return queryFirst(sql, params, mapResultMap);
/** {@inheritDoc} */
@Override
public List<Map<String, Object>> queryList(String sql)
throws SQLException {
try (Connection conn = this.dataSource.getConnection()) {
return JdbcOperationSupport
.queryList(conn, sql, ParamBuilder.EMPTY_OBJECT_ARRAY, RowMapper.HASH_MAP_MAPPER);
}
}
public static final ResultMap<DbRecord> recordResultMap = (rs, rowNumber) -> {
DbRecord result = new DbRecord();
ResultSetMetaData metaData = rs.getMetaData();
int columnCount = metaData.getColumnCount();
for (int i = 1; i <= columnCount; i++) {
String colName = metaData.getColumnName(i);
result.put(colName, rs.getObject(colName));
}
return result;
};
// #endregion
public List<DbRecord> queryToRecordList(String sql, Object... params) throws SQLException {
return query(sql, params, recordResultMap);
// #region - queryFirst
/** {@inheritDoc} */
@Override
public <T> Optional<T> queryFirst(String sql, Object[] params, RowMapper<T> rowMapper)
throws SQLException {
try (Connection conn = this.dataSource.getConnection()) {
final T result = JdbcOperationSupport.queryFirst(conn, sql, params, rowMapper);
return Optional.ofNullable(result);
}
}
public Optional<DbRecord> queryFirstRecord(String sql, Object... params) throws SQLException {
return queryFirst(sql, params, recordResultMap);
/** {@inheritDoc} */
@Override
public <T> Optional<T> queryFirst(String sql, Object[] params, Class<T> clazz)
throws SQLException {
try (Connection conn = this.dataSource.getConnection()) {
final T result = JdbcOperationSupport.queryFirst(conn, sql, params, clazz);
return Optional.ofNullable(result);
}
}
public Optional<String> queryToString(String sql, Object... params) throws SQLException {
return queryFirst(sql, params, (rs, rowNumber) -> rs.getString(1));
/** {@inheritDoc} */
@Override
public Optional<Map<String, Object>> queryFirst(String sql, Object[] params)
throws SQLException {
try (Connection conn = this.dataSource.getConnection()) {
final Map<String, Object> result = JdbcOperationSupport
.queryFirst(conn, sql, params, RowMapper.HASH_MAP_MAPPER);
return Optional.ofNullable(result);
}
}
public OptionalInt queryToInt(String sql, Object... params) throws SQLException {
Optional<Integer> result = queryFirst(sql, params, (rs, rowNumber) -> rs.getInt(1));
return OptionalTools.toOptionalInt(result);
/** {@inheritDoc} */
@Override
public Optional<String> queryFirstString(String sql, Object[] params)
throws SQLException {
try (Connection conn = this.dataSource.getConnection()) {
final String result = JdbcOperationSupport.queryFirstString(conn, sql, params);
return Optional.ofNullable(result);
}
}
public OptionalLong queryToLong(String sql, Object... params) throws SQLException {
Optional<Long> result = queryFirst(sql, params, (rs, rowNumber) -> rs.getLong(1));
return OptionalTools.toOptionalLong(result);
/** {@inheritDoc} */
@Override
public OptionalInt queryFirstInt(String sql, Object[] params)
throws SQLException {
try (Connection conn = this.dataSource.getConnection()) {
final Integer result = JdbcOperationSupport.queryFirstInt(conn, sql, params);
return OptionalTools.optionalOf(result);
}
}
public OptionalDouble queryToDouble(String sql, Object... params) throws SQLException {
Optional<Double> result = queryFirst(sql, params, (rs, rowNumber) -> rs.getDouble(1));
return OptionalTools.toOptionalDouble(result);
/** {@inheritDoc} */
@Override
public OptionalLong queryFirstLong(String sql, Object[] params)
throws SQLException {
try (Connection conn = this.dataSource.getConnection()) {
final Long result = JdbcOperationSupport.queryFirstLong(conn, sql, params);
return OptionalTools.optionalOf(result);
}
}
public Optional<BigDecimal> queryToBigDecimal(String sql, Object... params) throws SQLException {
return queryFirst(sql, params, (rs, rowNumber) -> rs.getBigDecimal(1));
/** {@inheritDoc} */
@Override
public OptionalDouble queryFirstDouble(String sql, Object[] params)
throws SQLException {
try (Connection conn = this.dataSource.getConnection()) {
final Double result = JdbcOperationSupport.queryFirstDouble(conn, sql, params);
return OptionalTools.optionalOf(result);
}
}
public int update(String sql, Object... params) throws SQLException {
try (PreparedStatement stmt = this.conn.prepareStatement(sql)) {
fillStatement(stmt, params);
return stmt.executeUpdate();
}
/** {@inheritDoc} */
@Override
public Optional<BigDecimal> queryFirstBigDecimal(String sql, Object[] params)
throws SQLException {
try (Connection conn = this.dataSource.getConnection()) {
final BigDecimal result = JdbcOperationSupport.queryFirstBigDecimal(conn, sql, params);
return Optional.ofNullable(result);
}
}
public int[] batchUpdate(String sql, Collection<Object[]> params, int batchSize) throws SQLException {
int executeCount = params.size() / batchSize;
executeCount = (params.size() % batchSize == 0) ? executeCount : (executeCount + 1);
List<int[]> result = Lists.newArrayListWithCapacity(executeCount);
try (PreparedStatement stmt = this.conn.prepareStatement(sql)) {
int i = 0;
for (Object[] ps : params) {
i++;
for (int j = 0; j < ps.length; j++) {
stmt.setObject(j + 1, ps[j]);
}
stmt.addBatch();
if (i % batchSize == 0 || i >= params.size()) {
int[] n = stmt.executeBatch();
result.add(n);
stmt.clearBatch();
}
}
return ArrayTools.concatIntArray(result);
}
/** {@inheritDoc} */
@Override
public <T> Optional<T> queryFirst(String sql, RowMapper<T> rowMapper)
throws SQLException {
try (Connection conn = this.dataSource.getConnection()) {
final T result = JdbcOperationSupport
.queryFirst(conn, sql, ParamBuilder.EMPTY_OBJECT_ARRAY, rowMapper);
return Optional.ofNullable(result);
}
}
public <E extends Exception> void tx(final IAtom<E> atom) throws SQLException, E {
Preconditions.checkNotNull(atom, "Atom can not be null.");
final boolean autoCommit = this.conn.getAutoCommit();
/** {@inheritDoc} */
@Override
public <T> Optional<T> queryFirst(String sql, Class<T> clazz)
throws SQLException {
try (Connection conn = this.dataSource.getConnection()) {
final T result = JdbcOperationSupport
.queryFirst(conn, sql, ParamBuilder.EMPTY_OBJECT_ARRAY, clazz);
return Optional.ofNullable(result);
}
}
/** {@inheritDoc} */
@Override
public Optional<Map<String, Object>> queryFirst(String sql)
throws SQLException {
try (Connection conn = this.dataSource.getConnection()) {
final Map<String, Object> result = JdbcOperationSupport
.queryFirst(conn, sql, ParamBuilder.EMPTY_OBJECT_ARRAY, RowMapper.HASH_MAP_MAPPER);
return Optional.ofNullable(result);
}
}
/** {@inheritDoc} */
@Override
public Optional<String> queryFirstString(String sql)
throws SQLException {
try (Connection conn = this.dataSource.getConnection()) {
final String result = JdbcOperationSupport.
queryFirstString(conn, sql, ParamBuilder.EMPTY_OBJECT_ARRAY);
return Optional.ofNullable(result);
}
}
/** {@inheritDoc} */
@Override
public OptionalInt queryFirstInt(String sql)
throws SQLException {
try (Connection conn = this.dataSource.getConnection()) {
final Integer result = JdbcOperationSupport
.queryFirstInt(conn, sql, ParamBuilder.EMPTY_OBJECT_ARRAY);
return OptionalTools.optionalOf(result);
}
}
/** {@inheritDoc} */
@Override
public OptionalLong queryFirstLong(String sql)
throws SQLException {
try (Connection conn = this.dataSource.getConnection()) {
final Long result = JdbcOperationSupport
.queryFirstLong(conn, sql, ParamBuilder.EMPTY_OBJECT_ARRAY);
return OptionalTools.optionalOf(result);
}
}
/** {@inheritDoc} */
@Override
public OptionalDouble queryFirstDouble(String sql)
throws SQLException {
try (Connection conn = this.dataSource.getConnection()) {
final Double result = JdbcOperationSupport
.queryFirstDouble(conn, sql, ParamBuilder.EMPTY_OBJECT_ARRAY);
return OptionalTools.optionalOf(result);
}
}
/** {@inheritDoc} */
@Override
public Optional<BigDecimal> queryFirstBigDecimal(String sql)
throws SQLException {
try (Connection conn = this.dataSource.getConnection()) {
final BigDecimal result = JdbcOperationSupport
.queryFirstBigDecimal(conn, sql, ParamBuilder.EMPTY_OBJECT_ARRAY);
return Optional.ofNullable(result);
}
}
/** {@inheritDoc} */
@Override
public boolean queryAsBoolean(String sql) // TODO 单元测试
throws SQLException {
try (Connection conn = this.dataSource.getConnection()) {
final Boolean result = JdbcOperationSupport
.queryFirstBoolean(conn, sql, ParamBuilder.EMPTY_OBJECT_ARRAY);
return Boolean.TRUE.equals(result);
}
}
/** {@inheritDoc} */
@Override
public boolean queryAsBoolean(String sql, Object[] params) // TODO 单元测试
throws SQLException {
try (Connection conn = this.dataSource.getConnection()) {
final Boolean result = JdbcOperationSupport
.queryFirstBoolean(conn, sql, params);
return Boolean.TRUE.equals(result);
}
}
// #endregion
// #region - update & batchUpdate
/** {@inheritDoc} */
@Override
public int update(String sql, Object[] params)
throws SQLException {
try (Connection conn = this.dataSource.getConnection()) {
return JdbcOperationSupport.update(conn, sql, params);
}
}
/** {@inheritDoc} */
@Override
public int update(String sql)
throws SQLException {
try (Connection conn = this.dataSource.getConnection()) {
return JdbcOperationSupport.update(conn, sql, ParamBuilder.EMPTY_OBJECT_ARRAY);
}
}
/** {@inheritDoc} */
@Override
public <T> List<T> update(String sql, Object[] params, RowMapper<T> rowMapper)
throws SQLException {
try (Connection conn = this.dataSource.getConnection()) {
return JdbcOperationSupport.update(conn, sql, params, rowMapper);
}
}
/** {@inheritDoc} */
@Override
public <T> List<T> update(String sql, RowMapper<T> rowMapper)
throws SQLException {
try (Connection conn = this.dataSource.getConnection()) {
return JdbcOperationSupport.update(conn, sql, ParamBuilder.EMPTY_OBJECT_ARRAY, rowMapper);
}
}
/** {@inheritDoc} */
@Override
public List<int[]> batchUpdate(String sql, @Nullable Collection<Object[]> params, int batchSize)
throws SQLException {
try (Connection conn = this.dataSource.getConnection()) {
return JdbcOperationSupport.batchUpdate(conn, sql, params, batchSize);
}
}
/** {@inheritDoc} */
@Override
public List<int[]> batchUpdateAndIgnoreException(String sql, @Nullable Collection<Object[]> params,
int batchSize, List<Exception> exceptions)
throws SQLException {
try (Connection conn = this.dataSource.getConnection()) {
return JdbcOperationSupport
.batchUpdateAndIgnoreException(conn, sql, params, batchSize, exceptions);
}
}
// #endregion
// #region - transaction
/**
* 执行事务。如果未发生异常,则提交事务;当有异常发生时,回滚事务
*
* <p>
* operations 中使用 JdbcExecutor 实参进行 JDBC 操作,这些操作在一个连接中
* </p>
*
* @param <E> 异常类型
* @param operations 事务操作
* @throws SQLException SQL 异常
* @throws E 事务中的异常
*/
public <E extends Exception> void executeTransaction(
@Nonnull final ThrowingConsumer<JdbcExecutor, E> operations)
throws SQLException, E {
AssertTools.checkNotNull(operations, "Operations can not be null.");
try (Connection conn = this.dataSource.getConnection()) {
final boolean autoCommit = conn.getAutoCommit();
try {
this.conn.setAutoCommit(false);
atom.execute(this);
this.conn.commit();
conn.setAutoCommit(false);
operations.accept(new JdbcExecutor(conn));
conn.commit();
}
catch (Exception e) {
this.conn.rollback();
conn.rollback();
throw e;
}
finally {
this.conn.setAutoCommit(autoCommit);
conn.setAutoCommit(autoCommit);
}
}
}
@FunctionalInterface
public interface IAtom<E extends Exception> {
void execute(JdbcExecutor jdbcExecutor) throws SQLException, E;
}
private static void fillStatement(PreparedStatement stmt, 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);
}
/**
* 执行事务。
* 如果 {@code operations} 返回 {@code true},则提交事务;
* 如果抛出异常,或返回 {@code false},则回滚事务
*
* @param <E> 事务中的异常
* @param operations 事务操作
* @throws SQLException 数据库异常
* @throws E 事务中的异常类型
*/
public <E extends Exception> void commitIfTrue(
@Nonnull final ThrowingPredicate<JdbcExecutor, E> operations)
throws SQLException, E {
AssertTools.checkNotNull(operations, "Operations can not be null.");
try (Connection conn = this.dataSource.getConnection()) {
final boolean autoCommit = conn.getAutoCommit();
try {
conn.setAutoCommit(false);
if (operations.test(new JdbcExecutor(conn))) {
conn.commit();
}
else {
conn.rollback();
}
}
catch (Exception e) {
conn.rollback();
throw e;
}
finally {
conn.setAutoCommit(autoCommit);
}
}
}
public static class ParamBuilder {
// #endregion
public static Object[] buildParams(final Object... params) {
if (ArrayUtils.isEmpty(params)) {
return ArrayUtils.EMPTY_OBJECT_ARRAY;
}
return Arrays.stream(params)
.map(param -> {
if (param instanceof Optional) {
return OptionalTools.orElseNull((Optional<?>) param);
}
if (param instanceof OptionalInt) {
return OptionalTools.toInteger(((OptionalInt) param));
}
if (param instanceof OptionalLong) {
return OptionalTools.toLong(((OptionalLong) param));
}
if (param instanceof OptionalDouble) {
return OptionalTools.toDouble(((OptionalDouble) param));
}
return param;
})
.toArray();
public static final class JdbcExecutor implements JdbcOperations {
private final Connection conn;
private JdbcExecutor(Connection conn) {
this.conn = conn;
}
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.");
if (CollectionTools.isEmpty(c)) {
return Collections.emptyList();
}
return c.stream().map(func).collect(Collectors.toList());
// #region - query
/** {@inheritDoc} */
@Override
public <T> T query(String sql, Object[] params, ResultHandler<T> resultHandler)
throws SQLException {
return JdbcOperationSupport.query(this.conn, sql, params, resultHandler);
}
private ParamBuilder() {
throw new IllegalStateException("Utility class");
/** {@inheritDoc} */
@Override
public <T> T query(String sql, ResultHandler<T> resultHandler)
throws SQLException {
return JdbcOperationSupport
.query(this.conn, sql, ParamBuilder.EMPTY_OBJECT_ARRAY, resultHandler);
}
// #endregion
// #region - queryList
/** {@inheritDoc} */
@Override
public <T> List<T> queryList(String sql, Object[] params, RowMapper<T> rowMapper)
throws SQLException {
return JdbcOperationSupport.queryList(this.conn, sql, params, rowMapper);
}
/** {@inheritDoc} */
@Override
public <T> List<T> queryList(String sql, Object[] params, Class<T> clazz)
throws SQLException {
return JdbcOperationSupport.queryList(this.conn, sql, params, clazz);
}
/** {@inheritDoc} */
@Override
public List<Map<String, Object>> queryList(String sql, Object[] params)
throws SQLException {
return JdbcOperationSupport.queryList(this.conn, sql, params, RowMapper.HASH_MAP_MAPPER);
}
/** {@inheritDoc} */
@Override
public <T> List<T> queryList(String sql, RowMapper<T> rowMapper)
throws SQLException {
return JdbcOperationSupport
.queryList(this.conn, sql, ParamBuilder.EMPTY_OBJECT_ARRAY, rowMapper);
}
/** {@inheritDoc} */
@Override
public <T> List<T> queryList(String sql, Class<T> clazz)
throws SQLException {
return JdbcOperationSupport.queryList(this.conn, sql, ParamBuilder.EMPTY_OBJECT_ARRAY, clazz);
}
/** {@inheritDoc} */
@Override
public List<Map<String, Object>> queryList(String sql)
throws SQLException {
return JdbcOperationSupport
.queryList(this.conn, sql, ParamBuilder.EMPTY_OBJECT_ARRAY, RowMapper.HASH_MAP_MAPPER);
}
// #endregion
// #region - queryFirst
/** {@inheritDoc} */
@Override
public <T> Optional<T> queryFirst(String sql, Object[] params, RowMapper<T> rowMapper)
throws SQLException {
final T result = JdbcOperationSupport.queryFirst(this.conn, sql, params, rowMapper);
return Optional.ofNullable(result);
}
/** {@inheritDoc} */
@Override
public <T> Optional<T> queryFirst(String sql, Object[] params, Class<T> clazz)
throws SQLException {
final T result = JdbcOperationSupport.queryFirst(this.conn, sql, params, clazz);
return Optional.ofNullable(result);
}
/** {@inheritDoc} */
@Override
public Optional<Map<String, Object>> queryFirst(String sql, Object[] params)
throws SQLException {
final Map<String, Object> result = JdbcOperationSupport
.queryFirst(this.conn, sql, params, RowMapper.HASH_MAP_MAPPER);
return Optional.ofNullable(result);
}
/** {@inheritDoc} */
@Override
public Optional<String> queryFirstString(String sql, Object[] params)
throws SQLException {
final String result = JdbcOperationSupport.queryFirstString(this.conn, sql, params);
return Optional.ofNullable(result);
}
/** {@inheritDoc} */
@Override
public OptionalInt queryFirstInt(String sql, Object[] params)
throws SQLException {
final Integer result = JdbcOperationSupport.queryFirstInt(this.conn, sql, params);
return OptionalTools.optionalOf(result);
}
/** {@inheritDoc} */
@Override
public OptionalLong queryFirstLong(String sql, Object[] params)
throws SQLException {
final Long result = JdbcOperationSupport.queryFirstLong(this.conn, sql, params);
return OptionalTools.optionalOf(result);
}
/** {@inheritDoc} */
@Override
public OptionalDouble queryFirstDouble(String sql, Object[] params)
throws SQLException {
final Double result = JdbcOperationSupport.queryFirstDouble(this.conn, sql, params);
return OptionalTools.optionalOf(result);
}
/** {@inheritDoc} */
@Override
public Optional<BigDecimal> queryFirstBigDecimal(String sql, Object[] params)
throws SQLException {
final BigDecimal result = JdbcOperationSupport.queryFirstBigDecimal(this.conn, sql, params);
return Optional.ofNullable(result);
}
/** {@inheritDoc} */
@Override
public <T> Optional<T> queryFirst(String sql, RowMapper<T> rowMapper)
throws SQLException {
final T result = JdbcOperationSupport
.queryFirst(this.conn, sql, ParamBuilder.EMPTY_OBJECT_ARRAY, rowMapper);
return Optional.ofNullable(result);
}
/** {@inheritDoc} */
@Override
public <T> Optional<T> queryFirst(String sql, Class<T> clazz)
throws SQLException {
final T result = JdbcOperationSupport
.queryFirst(this.conn, sql, ParamBuilder.EMPTY_OBJECT_ARRAY, clazz);
return Optional.ofNullable(result);
}
/** {@inheritDoc} */
@Override
public Optional<Map<String, Object>> queryFirst(String sql)
throws SQLException {
final Map<String, Object> result = JdbcOperationSupport
.queryFirst(this.conn, sql, ParamBuilder.EMPTY_OBJECT_ARRAY, RowMapper.HASH_MAP_MAPPER);
return Optional.ofNullable(result);
}
/** {@inheritDoc} */
@Override
public Optional<String> queryFirstString(String sql)
throws SQLException {
final String result = JdbcOperationSupport
.queryFirstString(this.conn, sql, ParamBuilder.EMPTY_OBJECT_ARRAY);
return Optional.ofNullable(result);
}
/** {@inheritDoc} */
@Override
public OptionalInt queryFirstInt(String sql)
throws SQLException {
final Integer result = JdbcOperationSupport
.queryFirstInt(this.conn, sql, ParamBuilder.EMPTY_OBJECT_ARRAY);
return OptionalTools.optionalOf(result);
}
/** {@inheritDoc} */
@Override
public OptionalLong queryFirstLong(String sql)
throws SQLException {
final Long result = JdbcOperationSupport
.queryFirstLong(this.conn, sql, ParamBuilder.EMPTY_OBJECT_ARRAY);
return OptionalTools.optionalOf(result);
}
/** {@inheritDoc} */
@Override
public OptionalDouble queryFirstDouble(String sql)
throws SQLException {
final Double result = JdbcOperationSupport
.queryFirstDouble(this.conn, sql, ParamBuilder.EMPTY_OBJECT_ARRAY);
return OptionalTools.optionalOf(result);
}
/** {@inheritDoc} */
@Override
public Optional<BigDecimal> queryFirstBigDecimal(String sql)
throws SQLException {
final BigDecimal result = JdbcOperationSupport
.queryFirstBigDecimal(this.conn, sql, ParamBuilder.EMPTY_OBJECT_ARRAY);
return Optional.ofNullable(result);
}
/** {@inheritDoc} */
@Override
public boolean queryAsBoolean(String sql)
throws SQLException {
final Boolean result = JdbcOperationSupport
.queryFirstBoolean(this.conn, sql, ParamBuilder.EMPTY_OBJECT_ARRAY);
return Boolean.TRUE.equals(result);
}
/** {@inheritDoc} */
@Override
public boolean queryAsBoolean(String sql, Object[] params)
throws SQLException {
final Boolean result = JdbcOperationSupport.queryFirstBoolean(this.conn, sql, params);
return Boolean.TRUE.equals(result);
}
// #endregion
// #region - update & batchUpdate
/** {@inheritDoc} */
@Override
public int update(String sql, Object[] params)
throws SQLException {
return JdbcOperationSupport.update(this.conn, sql, params);
}
/** {@inheritDoc} */
@Override
public int update(String sql)
throws SQLException {
return JdbcOperationSupport.update(this.conn, sql, ParamBuilder.EMPTY_OBJECT_ARRAY);
}
/** {@inheritDoc} */
@Override
public <T> List<T> update(String sql, Object[] params, RowMapper<T> rowMapper)
throws SQLException {
return JdbcOperationSupport.update(this.conn, sql, params, rowMapper);
}
/** {@inheritDoc} */
@Override
public <T> List<T> update(String sql, RowMapper<T> rowMapper)
throws SQLException {
return JdbcOperationSupport.update(this.conn, sql, ParamBuilder.EMPTY_OBJECT_ARRAY, rowMapper);
}
/** {@inheritDoc} */
@Override
public List<int[]> batchUpdate(String sql, @Nullable Collection<Object[]> params, int batchSize)
throws SQLException {
return JdbcOperationSupport.batchUpdate(this.conn, sql, params, batchSize);
}
/** {@inheritDoc} */
@Override
public List<int[]> batchUpdateAndIgnoreException(String sql,
@Nullable Collection<Object[]> params,
int batchSize,
List<Exception> exceptions) throws SQLException {
return JdbcOperationSupport
.batchUpdateAndIgnoreException(this.conn, sql, params, batchSize, exceptions);
}
// #endregion
}
}

View File

@@ -1,124 +0,0 @@
package xyz.zhouxy.jdbc;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static xyz.zhouxy.jdbc.SimpleJdbcTemplate.ParamBuilder.*;
import static xyz.zhouxy.plusone.commons.sql.JdbcSql.IN;
import java.sql.Connection;
import java.sql.SQLException;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import javax.sql.DataSource;
import org.junit.jupiter.api.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
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;
class SimpleJdbcTemplateTests {
private static final Logger log = LoggerFactory.getLogger(SimpleJdbcTemplateTests.class);
private static final DataSource dataSource;
String[] cStruct = {
"id",
"created_by",
"create_time",
"updated_by",
"update_time",
"status"
};
static {
HikariConfig config = new HikariConfig();
config.setJdbcUrl("jdbc:postgresql://localhost:5432/plusone");
config.setUsername("postgres");
config.setPassword("zhouxy108");
config.setMaximumPoolSize(800);
config.setConnectionTimeout(1000000);
dataSource = new HikariDataSource(config);
}
@Test
void testQuery() throws SQLException {
try (Connection conn = dataSource.getConnection()) {
Object[] ids = buildParams("501533", "501554", "544599");
String sql = SQL.newJdbcSql()
.SELECT("*")
.FROM("test_table")
.WHERE(IN("id", ids))
.toString();
log.info(sql);
List<DbRecord> rs = SimpleJdbcTemplate.connect(conn)
.queryToRecordList(sql, ids);
assertNotNull(rs);
for (DbRecord baseEntity : rs) {
// log.info("id: {}", baseEntity.getValueAsString("id"));
log.info(baseEntity.toString());
assertEquals(Optional.empty(), baseEntity.getValueAsString("updated_by"));
}
}
}
final IdWorker idGenerator = IdGenerator.getSnowflakeIdGenerator(0);
@Test
void testTransaction() throws SQLException {
try (Connection conn = dataSource.getConnection()) {
long id = this.idGenerator.nextId();
JdbcExecutor jdbcExecutor = SimpleJdbcTemplate.connect(conn);
try {
jdbcExecutor.tx(jdbc -> {
jdbc.update("INSERT INTO base_table (id, created_by, create_time, status) VALUES (?, ?, ?, ?)",
buildParams(id, 585757, LocalDateTime.now(), 0));
throw new NullPointerException();
});
}
catch (NullPointerException e) {
// ignore
}
catch (Exception e) {
e.printStackTrace();
}
Optional<Map<String, Object>> first = jdbcExecutor
.queryFirst("SELECT * FROM base_table WHERE id = ?", id);
log.info("first: {}", first);
assertTrue(!first.isPresent());
}
try (Connection conn = dataSource.getConnection()) {
long id = this.idGenerator.nextId();
JdbcExecutor jdbcExecutor = SimpleJdbcTemplate.connect(conn);
try {
jdbcExecutor.tx(jdbc -> {
jdbc.update("INSERT INTO base_table (id, created_by, create_time, status) VALUES (?, ?, ?, ?)",
buildParams(id, 585757, LocalDateTime.now(), 0));
// throw new NullPointerException();
});
}
catch (NullPointerException e) {
// ignore
}
catch (Exception e) {
e.printStackTrace();
}
Optional<Map<String, Object>> first = jdbcExecutor
.queryFirst("SELECT * FROM base_table WHERE id = ?", id);
log.info("first: {}", first);
assertTrue(first.isPresent());
}
}
}

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

@@ -0,0 +1,246 @@
/*
* 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.buildParams;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.sql.SQLException;
import java.time.LocalDateTime;
import java.util.Date;
import java.util.List;
import java.util.Map;
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.io.Resources;
import xyz.zhouxy.jdbc.RowMapper;
import xyz.zhouxy.jdbc.SimpleJdbcTemplate;
import xyz.zhouxy.jdbc.SimpleJdbcTemplate.JdbcExecutor;
import xyz.zhouxy.plusone.commons.util.IdGenerator;
import xyz.zhouxy.plusone.commons.util.IdWorker;
class SimpleJdbcTemplateTests {
private static final Logger log = LoggerFactory.getLogger(SimpleJdbcTemplateTests.class);
private static SimpleJdbcTemplate jdbcTemplate;
@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");
}
@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 = "SELECT id, username, account_status FROM sys_account WHERE id IN (?, ?, ?, ?, ?, ?, ?)";
log.info(sql);
List<Map<String, Object>> rs = jdbcTemplate.queryList(sql, ids);
for (Map<String, Object> dbRecord : rs) {
log.info("{}", dbRecord);
}
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
void testInsert() throws SQLException {
List<Map<String, Object>> keys = jdbcTemplate.update(
"INSERT INTO sys_account(username, account_status, created_by) VALUES (?, ?, ?), (?, ?, ?)",
buildParams("zhouxy21", "2", 123L, "code22", '2', 456L),
RowMapper.HASH_MAP_MAPPER);
log.info("keys: {}", keys);
assertEquals(2, keys.size());
for (Map<String,Object> key : keys) {
assertTrue(key.containsKey("id"));
assertInstanceOf(Long.class, key.get("id"));
assertTrue(key.containsKey("create_time"));
assertInstanceOf(Date.class, key.get("create_time"));
}
List<Long> ids = jdbcTemplate.update(
"INSERT INTO sys_account(username, account_status, created_by) VALUES (?, ?, ?), (?, ?, ?)",
buildParams("zhouxy21", "2", 123L, "code22", '2', 456L),
(rs, rowNumber) -> rs.getObject("id", Long.class));
log.info("ids: {}", ids);
assertEquals(2, ids.size());
}
@Test
void testUpdate() throws SQLException {
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.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.HASH_MAP_MAPPER);
assertEquals(0, keys.size());
}
final IdWorker idGenerator = IdGenerator.getSnowflakeIdGenerator(0);
@Test
void testTransaction() throws SQLException {
// 抛异常,回滚
{
long id = this.idGenerator.nextId();
try {
jdbcTemplate.executeTransaction((JdbcExecutor jdbc) -> {
jdbc.update("INSERT INTO sys_account (id, username, created_by, create_time, account_status) VALUES (?, ?, ?, ?, ?)",
buildParams(id, "testTransaction1", 100, LocalDateTime.now(), "55"));
throw new NullPointerException();
});
}
catch (NullPointerException e) {
// ignore
}
Optional<Map<String, Object>> first = jdbcTemplate
.queryFirst("SELECT * FROM sys_account WHERE id = ?", buildParams(id));
log.info("first: {}", first);
assertFalse(first.isPresent());
}
// 没有异常,提交事务
{
long id = this.idGenerator.nextId();
jdbcTemplate.executeTransaction(jdbc -> {
jdbc.update("INSERT INTO sys_account (id, username, created_by, create_time, account_status) VALUES (?, ?, ?, ?, ?)",
buildParams(id, "testTransaction2", 101, LocalDateTime.now(), "55"));
});
Optional<Map<String, Object>> first = jdbcTemplate
.queryFirst("SELECT * FROM sys_account WHERE id = ?", buildParams(id));
log.info("first: {}", first);
assertTrue(first.isPresent());
}
// 抛异常,回滚
{
long id = this.idGenerator.nextId();
try {
jdbcTemplate.commitIfTrue(jdbc -> {
jdbc.update("INSERT INTO sys_account (id, username, created_by, create_time, account_status) VALUES (?, ?, ?, ?, ?)",
buildParams(id, "testTransaction3", 102, LocalDateTime.now(), "55"));
throw new NullPointerException();
});
}
catch (NullPointerException e) {
// ignore
}
Optional<Map<String, Object>> first = jdbcTemplate
.queryFirst("SELECT * FROM sys_account WHERE id = ?", buildParams(id));
log.info("first: {}", first);
assertFalse(first.isPresent());
}
// 返回 false回滚
{
long id = this.idGenerator.nextId();
jdbcTemplate.commitIfTrue(jdbc -> {
jdbc.update("INSERT INTO sys_account (id, username, created_by, create_time, account_status) VALUES (?, ?, ?, ?, ?)",
buildParams(id, "testTransaction4", 103, LocalDateTime.now(), "55"));
return false;
});
Optional<Map<String, Object>> first = jdbcTemplate
.queryFirst("SELECT * FROM sys_account WHERE id = ?", buildParams(id));
log.info("first: {}", first);
assertFalse(first.isPresent());
}
// 返回 true提交事务
{
long id = this.idGenerator.nextId();
jdbcTemplate.commitIfTrue(jdbc -> {
jdbc.update("INSERT INTO sys_account (id, username, created_by, create_time, account_status) VALUES (?, ?, ?, ?, ?)",
buildParams(id, "testTransaction5", 104, LocalDateTime.now(), "55"));
return true;
});
Optional<Map<String, Object>> first = jdbcTemplate
.queryFirst("SELECT * FROM sys_account WHERE id = ?", buildParams(id));
log.info("first: {}", first);
assertTrue(first.isPresent());
}
}
@Test
void testBean() throws Exception {
Optional<AccountPO> t = jdbcTemplate.queryFirst(
"SELECT * FROM sys_account WHERE id = ?",
buildParams(18L),
RowMapper.beanRowMapper(AccountPO.class));
assertEquals(
new AccountPO(18L, "zhouxy18", "1",
LocalDateTime.of(2000, 1, 1, 0, 0), 118L,
LocalDateTime.of(2000, 1, 29, 0, 0), null, 7L),
t.get());
log.info("{}", t);
}
}

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
)